From 731096a6f8e5a14f8e6994fbf70de6d58b522a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Sun, 9 Aug 2026 23:30:25 -0400 Subject: [PATCH] fix: use the extended protocol for unknown models that answer 0xEA 0x81 Protocol probing passes the probe protocol in as the fallback for model numbers missing from models_db. When a device answers the state query with the extended 0xEA 0x81 frame, that fallback is ProtocolLEDENET8Byte, which reports extended responses as valid via is_valid_extended_state_response but never overrides extended_state_to_state. The abstract stub returns None, so named_raw_state unpacks None and data_received dies with: TypeError: LEDENETRawState() argument after * must be an iterable, not NoneType Home Assistant surfaces this as "Cannot determine protocol" on repeat. Select PROTOCOL_LEDENET_EXTENDED_CUSTOM as the fallback when the frame that identified the device was an extended one, so an unknown model is driven by a protocol that can parse what it actually sent. Known models are unaffected: the fallback is only consulted when get_model finds no entry. Verified against a Surplife AK001-ZJ21413 reporting model_num 0x77, which never sends the 14-byte response. With this change and no models_db entry it sets up as LEDENET_EXTENDED_CUSTOM, and power, RGB and brightness all work. Co-Authored-By: Claude Opus 5 (1M context) --- flux_led/base_device.py | 8 ++++++++ tests/test_aio.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/flux_led/base_device.py b/flux_led/base_device.py index f17e9208..dcd3bc76 100644 --- a/flux_led/base_device.py +++ b/flux_led/base_device.py @@ -1279,6 +1279,14 @@ def _set_protocol_from_msg( # Extended state format (0xEA 0x81): model at byte 4, version at byte 5. self._model_num = full_msg[4] version_num = full_msg[5] + # A device that answers with the extended format cannot be driven by + # the 8-byte protocol: ProtocolLEDENET8Byte reports these responses as + # valid but never implements extended_state_to_state, so the abstract + # stub returns None and unpacking it raises TypeError. Probing hands us + # the probe protocol as the fallback, which is only used for model + # numbers missing from models_db, so point unknown models at a protocol + # that can actually parse what the device just sent. + fallback_protocol = PROTOCOL_LEDENET_EXTENDED_CUSTOM else: # Standard state format (0x81): model at byte 1, version at byte 10. self._model_num = full_msg[1] diff --git a/tests/test_aio.py b/tests/test_aio.py index dd9ff006..9f16736e 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -4244,6 +4244,35 @@ async def test_extended_state_color_parsing( ) +@pytest.mark.asyncio +async def test_setup_unknown_model_that_only_speaks_extended_state(mock_aio_protocol): + """An unknown model_num answering only 0xEA 0x81 must still set up. + + Captured from a Surplife AK001-ZJ21413 reporting model_num 0x77, which never + sends the 14-byte state response. Probing hands the 8-byte protocol in as the + fallback for unknown models, but ProtocolLEDENET8Byte reports extended + responses as valid while inheriting the abstract extended_state_to_state, so + unpacking its None result raised TypeError inside data_received. + """ + light = AIOWifiLedBulb("192.168.1.166", timeout=0.01) + + def _updated_callback(*args, **kwargs): + pass + + task = asyncio.create_task(light.async_setup(_updated_callback)) + await mock_aio_protocol() + light._aio_protocol.data_received( + b"\xea\x81\x01\x00\x77\x09\x23\x25\x03\x50\xf0\x0b\xe4\x64" + b"\x00\x00\x01\x00\x64\x00\x00\x00\x80\x03\x00\x00\x00" + ) + await task + + assert light.available + assert light.model_num == 0x77 + assert light.protocol == PROTOCOL_LEDENET_EXTENDED_CUSTOM + assert light.is_on is True + + @pytest.mark.asyncio async def test_setup_0x35_with_version_num_10( mock_aio_protocol, caplog: pytest.LogCaptureFixture