From f58575412de8d3c6c2af37839546cb1ce73ce7c8 Mon Sep 17 00:00:00 2001 From: Derek Kaye Date: Fri, 3 Jul 2026 13:44:17 +0100 Subject: [PATCH 1/6] fix: Add failing tests to prove that X1HybridGen4 payloads are inocrrectly detected. These are detected as multiple inverters - both X1LiteLV and X1HybridGen4 and X1LiteLV is errorneously returned first. --- tests/test_discovery.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 3a2f956..cbb68d3 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -6,7 +6,7 @@ from solax import InverterResponse from solax.discovery import REGISTRY, DiscoveryError from solax.inverter import InverterError -from solax.inverters import X1Boost +from solax.inverters import X1Boost, X1HybridGen4, X1LiteLV class DelayedX1Boost(X1Boost): @@ -34,6 +34,41 @@ async def test_discovery(inverters_fixture): assert data.serial_number == data.dongle_serial_number +@pytest.mark.asyncio +async def test_discovery_x1_hybrid_gen4_is_not_misdetected_as_x1_lite_lv(inverters_fixture): + conn, inverter_class, _ = inverters_fixture + + if inverter_class is not X1HybridGen4: + pytest.skip() + + inverters = await solax.discover( + *conn, + inverters=[X1HybridGen4, X1LiteLV], + return_when=asyncio.ALL_COMPLETED, + ) + discovered_types = {type(inverter) for inverter in inverters} + + assert discovered_types == {X1HybridGen4} + + +@pytest.mark.asyncio +async def test_discovery_first_completed_prefers_correct_model_for_x1_hybrid_gen4( + inverters_fixture, +): + conn, inverter_class, _ = inverters_fixture + + if inverter_class is not X1HybridGen4: + pytest.skip() + + inverter = await solax.discover( + *conn, + inverters=[X1LiteLV, X1HybridGen4], + return_when=asyncio.FIRST_COMPLETED, + ) + + assert type(inverter) is X1HybridGen4 + + @pytest.mark.asyncio async def test_real_time_api(inverters_fixture): conn, inverter_class, _ = inverters_fixture From e1df8d6aea49be9fa930d9b73557e77854ad1270 Mon Sep 17 00:00:00 2001 From: Derek Kaye Date: Fri, 3 Jul 2026 14:16:35 +0100 Subject: [PATCH 2/6] fix: Make test more generic rather focussing on just my own case --- tests/test_discovery.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index cbb68d3..92b3de5 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -35,38 +35,44 @@ async def test_discovery(inverters_fixture): @pytest.mark.asyncio -async def test_discovery_x1_hybrid_gen4_is_not_misdetected_as_x1_lite_lv(inverters_fixture): +async def test_discovery_returns_only_expected_model_for_overlapping_schemas( + inverters_fixture, +): conn, inverter_class, _ = inverters_fixture - if inverter_class is not X1HybridGen4: + overlapping_inverters = {X1HybridGen4, X1LiteLV} + + if inverter_class not in overlapping_inverters: pytest.skip() inverters = await solax.discover( *conn, - inverters=[X1HybridGen4, X1LiteLV], + inverters=list(overlapping_inverters), return_when=asyncio.ALL_COMPLETED, ) discovered_types = {type(inverter) for inverter in inverters} - assert discovered_types == {X1HybridGen4} + assert discovered_types == {inverter_class} @pytest.mark.asyncio -async def test_discovery_first_completed_prefers_correct_model_for_x1_hybrid_gen4( +async def test_discovery_first_completed_returns_expected_model_for_overlapping_schemas( inverters_fixture, ): conn, inverter_class, _ = inverters_fixture - if inverter_class is not X1HybridGen4: + overlapping_inverters = [X1LiteLV, X1HybridGen4] + + if inverter_class not in set(overlapping_inverters): pytest.skip() inverter = await solax.discover( *conn, - inverters=[X1LiteLV, X1HybridGen4], + inverters=overlapping_inverters, return_when=asyncio.FIRST_COMPLETED, ) - assert type(inverter) is X1HybridGen4 + assert type(inverter) is inverter_class @pytest.mark.asyncio From 2c4dd717fa11395423e7558dc3200041e8ff9a51 Mon Sep 17 00:00:00 2001 From: Derek Kaye Date: Fri, 3 Jul 2026 14:17:53 +0100 Subject: [PATCH 3/6] fix: Specify the type ID for X1LiteLV This stops it detecting multiple inverters when both have an integer type. I don't know if this will break the X1LiteLV type though --- solax/inverters/x1_lite_lv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solax/inverters/x1_lite_lv.py b/solax/inverters/x1_lite_lv.py index 5bb0638..20a381d 100644 --- a/solax/inverters/x1_lite_lv.py +++ b/solax/inverters/x1_lite_lv.py @@ -11,7 +11,7 @@ class X1LiteLV(Inverter): # pylint: disable=duplicate-code _schema = vol.Schema( { - vol.Required("type"): int, + vol.Required("type"): vol.All(int, 103), vol.Required("sn"): str, vol.Required("ver"): str, vol.Required("data"): vol.Schema( From d4c8306c8486e5d63da12cbe80ccfdece70ac66d Mon Sep 17 00:00:00 2001 From: Derek Kaye Date: Fri, 3 Jul 2026 14:27:15 +0100 Subject: [PATCH 4/6] fix: styling with black --- tests/test_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 92b3de5..5d5e3e9 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -152,7 +152,7 @@ async def test_discovery_not_first_completed_after_staggering( inverters = await solax.discover( *conn, inverters=[DelayedX1Boost, DelayedFailedX1Boost], - return_when=asyncio.FIRST_EXCEPTION + return_when=asyncio.FIRST_EXCEPTION, ) assert DelayedX1Boost in {type(inverter) for inverter in inverters} From 838a5ebb19ccd1174112d924af0bab008ef178da Mon Sep 17 00:00:00 2001 From: Derek Kaye Date: Fri, 3 Jul 2026 14:44:38 +0100 Subject: [PATCH 5/6] fix: styling fixes from PR #210 --- solax/inverter.py | 2 +- solax/response_parser.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/solax/inverter.py b/solax/inverter.py index 8abe642..8f24814 100644 --- a/solax/inverter.py +++ b/solax/inverter.py @@ -91,7 +91,7 @@ def sensor_map(cls) -> Dict[str, Tuple[int, Measurement]]: for name, mapping in cls.response_decoder().items(): unit = Measurement(Units.NONE) - (idx, unit_or_measurement, *_) = mapping + idx, unit_or_measurement, *_ = mapping if isinstance(unit_or_measurement, Units): unit = Measurement(unit_or_measurement) diff --git a/solax/response_parser.py b/solax/response_parser.py index cbccbdd..6665bd4 100644 --- a/solax/response_parser.py +++ b/solax/response_parser.py @@ -46,7 +46,7 @@ def serial_number(self): _KEY_TYPE = "type" -GenericResponseSchema = vol.All( +GENERIC_RESPONSE_SCHEMA = vol.All( vol.Schema({vol.Required(_KEY_SERIAL): str}, extra=vol.ALLOW_EXTRA), vol.Any( vol.Schema({vol.Required(_KEY_VERSION): str}, extra=vol.ALLOW_EXTRA), @@ -77,7 +77,7 @@ def __init__( dongle_serial_number_getter: Callable[[Dict[str, Any]], Optional[str]], inverter_serial_number_getter: Callable[[Dict[str, Any]], Optional[str]], ) -> None: - self.schema = vol.And(GenericResponseSchema, schema) + self.schema = vol.And(GENERIC_RESPONSE_SCHEMA, schema) self.response_decoder = decoder self.dongle_serial_number_getter = dongle_serial_number_getter self.inverter_serial_number_getter = inverter_serial_number_getter @@ -95,7 +95,7 @@ def _postprocess_gen( Return map of functions to be applied to each sensor value """ for name, mapping in self.response_decoder.items(): - (_, _, *processors) = mapping + _, _, *processors = mapping for processor in processors: yield name, processor From deb65b0dec0c9382f39d3b48dc37bfa9b800622e Mon Sep 17 00:00:00 2001 From: Derek Kaye Date: Fri, 3 Jul 2026 14:53:14 +0100 Subject: [PATCH 6/6] fix: linting error --- tests/test_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 5d5e3e9..5d33c4a 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -72,7 +72,7 @@ async def test_discovery_first_completed_returns_expected_model_for_overlapping_ return_when=asyncio.FIRST_COMPLETED, ) - assert type(inverter) is inverter_class + assert isinstance(inverter, inverter_class) @pytest.mark.asyncio