Skip to content

fix: use the extended protocol for unknown models that answer 0xEA 0x81 - #555

Open
dccote wants to merge 1 commit into
lightinglibs:masterfrom
dccote:fix/extended-state-unknown-model
Open

fix: use the extended protocol for unknown models that answer 0xEA 0x81#555
dccote wants to merge 1 commit into
lightinglibs:masterfrom
dccote:fix/extended-state-unknown-model

Conversation

@dccote

@dccote dccote commented Aug 10, 2026

Copy link
Copy Markdown

Problem

Protocol probing passes the probe protocol in as the fallback_protocol 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 in ProtocolBase returns None, so named_raw_state() unpacks None and the receive callback dies:

Fatal error: protocol.data_received() call failed.
  File "flux_led/aiodevice.py", line 743, in _async_process_message
    self.process_extended_state_response(msg)
  File "flux_led/base_device.py", line 885, in process_extended_state_response
    self._process_valid_state_response(self._protocol.extended_state_to_state(msg))
  File "flux_led/protocol.py", line 1128, in named_raw_state
    return LEDENETRawState(*raw_state)
TypeError: flux_led.protocol.LEDENETRawState() argument after * must be an iterable, not NoneType

Home Assistant surfaces this as a setup loop:

Failed setup, will retry: 192.168.2.237: Cannot determine protocol

Fix

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: fallback_protocol is only consulted when get_model() finds no models_db entry.

This is deliberately not a per-model models_db addition. AK001-ZJ21413 hardware is already showing up under at least three different model numbers: 0xB6 (supported since #538/#539), 0x82 (reported in #547, still unsupported) and 0x77 (mine). Adding entries one at a time leaves the next variant with the same crash, whereas any device that only speaks the extended format now degrades to a working generic device instead of failing setup.

Verification

Hardware. A Surplife AK001-ZJ21413 reporting model_num 0x77, firmware 77_24_20240423_ZG-BK. It never sends the 14-byte state response, only:

ea 81 01 00 77 09 23 25 03 50 f0 0b e4 64 00 00 01 00 64 00 00 00 80 03 00 00 00

With this change and no models_db entry:

protocol    : LEDENET_EXTENDED_CUSTOM
model_num   : 0x77
model       : Unknown Model (0x77)
color_modes : {'RGB'}
effects     : 21

Power, RGB and brightness all verified end to end, each set confirmed by reading the state back, and the original preset pattern restored afterwards.

Tests. Added test_setup_unknown_model_that_only_speaks_extended_state, built from the real frame above. It fails on master with exactly the TypeError shown, and passes with the fix. Full suite goes from 151 to 152 passing. ruff check, ruff format and mypy are clean.

Note on #547

This does not give #547 its white channel back, since that device reports LEDENET_8BYTE with a standard state response and needs a proper WRGB models_db entry. I mention it only because it is the same AK001-ZJ21413 hardware string under yet another model number, which is what motivated fixing the generic path rather than adding 0x77 to the table.

Why not a dedicated models_db entry for 0x77

I measured that option too, by cloning the 0xB6 entry onto 0x77. It does advertise more: {'RGB', 'DIM'} and 22 effects, against {'RGB'} and 21 for the generic fallback. But the extra white channel is not real on this unit. Driving w=255 flips color_mode to DIM while the white bytes of the extended frame stay flat and the fixture goes dark:

start        mode=RGB   rgb=(255, 0, 0)   raw[14]=0 raw[15]=0
w=255        mode=DIM   rgb=(0, 0, 0)     raw[14]=0 raw[15]=0
w=0          mode=RGB   rgb=(0, 0, 0)     raw[14]=0 raw[15]=0

raw[14] (white temperature) and raw[15] (white brightness) never move. So a 0xB6-shaped entry would advertise a capability this hardware does not have, and hand users a white mode that silently blanks the light. The generic RGB fallback that this PR produces is the more accurate description of the device.

That is a further argument for fixing the generic path rather than growing the table: the variants of this product differ in what they can actually do, so a copied entry is as likely to overstate a device as to help it.

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) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
flux_led/base_device.py 96.38% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dccote

dccote commented Aug 10, 2026

Copy link
Copy Markdown
Author

Correcting something I offered in the original description, before anyone spends review time on it.

I had said I would happily follow up with a dedicated models_db entry for 0x77. Having now measured it on the device, I no longer think that entry should exist, and I have replaced that paragraph in the description with the data.

Cloning the 0xB6 entry onto 0x77 advertises {'RGB', 'DIM'} and 22 effects instead of {'RGB'} and 21, but the white channel it promises is not wired up on this unit:

start        mode=RGB   rgb=(255, 0, 0)   raw[14]=0 raw[15]=0
w=255        mode=DIM   rgb=(0, 0, 0)     raw[14]=0 raw[15]=0
w=0          mode=RGB   rgb=(0, 0, 0)     raw[14]=0 raw[15]=0

raw[14] (white temperature) and raw[15] (white brightness) never move, and the fixture just goes dark. So the copied entry would hand users a white mode in Home Assistant that silently blanks the light, which is worse than the generic RGB fallback this PR already gives them.

I think that strengthens the case for the change as it stands. The AK001-ZJ21413 variants differ in what they can actually do, not just in their model number, so copying a neighbour's entry can overstate a device as easily as it can help it. Getting the generic path to pick a protocol that can parse what the device sent leaves every variant working, and leaves the table free to describe only devices someone has actually verified.

No code change in this update, description only. CI was green on the current commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant