feat: extended custom effects, segment colors, and scribble (per-LED) for 0xB6#540
feat: extended custom effects, segment colors, and scribble (per-LED) for 0xB6#540asxzy wants to merge 12 commits into
Conversation
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
134fd15 to
2546623
Compare
- const.py: ExtendedCustomEffectPattern / Direction / Option enums - pattern.py: EXTENDED_CUSTOM_EFFECT_ID_NAME / NAME_ID maps - protocol.py: construct_extended_custom_effect (0xE1 0x21), construct_custom_segment_colors (0xE1 0x22), construct_levels_change and the HSV(+W) byte helpers on ProtocolLEDENETExtendedCustom - base_device.py: extended_custom_effect_pattern_list, _generate_* helpers and pattern-name lookup in _named_effect - aiodevice.py / device.py: async_set_* and set* entry points Stacked on the dedicated-protocol PR.
…test, and dedup HSV helper for 0xB6 - Guard setExtendedCustomEffect/setCustomSegmentColors/setScribble (sync) and async_set_extended_custom_effect/async_set_custom_segment_colors/ async_set_scribble (async) with a clear ValueError on unsupported devices, instead of hitting a bare assert (AssertionError, or AttributeError under -O). - Add golden inner-byte test for construct_custom_segment_colors pinning the E1 22 header, 0x14 count, per-segment HSV, and 20-segment padding. - Add sync test_0xB6_setScribble exercising the E1 23 init + E1 26 paint loop. - Collapse _rgb_to_hsv_bytes to delegate to _rgb_to_hsv_bytes_rgbw(r,g,b,0).
2546623 to
e81e97b
Compare
|
|
bluetoothbot
left a comment
There was a problem hiding this comment.
Blocking issues found.
- Pattern ID validation is over-permissive: accepts 23–24 which have no defined pattern
…e direction, guard led_count==0, and cover validation guards (0xB6 review)
|
|
bluetoothbot
left a comment
There was a problem hiding this comment.
Blocking issues found.
- Animation params (speed/density/direction/option) bypass range validation
|
Will add parameter validation to the animations tomorrow. |
… and surface unmapped effect modes (0xB6 review)
Confirmed present at this head. |
|
|
bluetoothbot
left a comment
There was a problem hiding this comment.
Blocking issues found.
- Solid color (mode 0x6E) is reported as an active effect, distorting
effect/brightness
…fect, drop phantom effect-name entries, and match app E1 21 header byte (0xB6 review, hardware-verified)
…stead of E1 21 Static Fill, so a plain color-set reports as a color not an effect (hardware-verified)
Use the exact EA81 frames captured from the device for the Colorful/Scenes state-reporting tests (Wave and Static Fill now use the real speed bytes; solid full/dim were already real), and note that the construct_levels_change color golden is byte-identical to the app's captured 'Colorful -> red' E1 22 packet.
_generate_levels_change mirrors a single white value into both warm and cool for non-CCT devices; the 0xB6 override summed them, doubling the white level (setWarmWhite(50%) shipped full brightness). Combine with max instead of sum (the device has one white LED). Verified on hardware: white now tracks the requested level linearly. Repurpose the old clamping test as a double-count regression test.
|
|
bluetoothbot
left a comment
There was a problem hiding this comment.
Blocking issues found.
- Extended-effect API rejects the enums this PR ships (pattern_id/direction/option are int-only)
…init send, and report unmapped 0xB6 scene modes distinctly (review)
|
Thanks — addressed in
|
Verified all three at head. Enum coercion present. Init now after validation. Unknown placeholder returned. One gap remains: |
|
|
bluetoothbot
left a comment
There was a problem hiding this comment.
Blocking issues found.
- effect_list omits the extended custom effects that
effectnow reports (0xB6) - Two new public naming APIs disagree on case:
extended_custom_effect_pattern_listvs reportedeffect
…coalesce non-blinking scribble groups, and narrow effect enum coercion (review)
…integration) The prior change advertised extended custom effect names in effect_list, but set_effect(name, speed) can't set them (they require a color list), so every listed name was unsettable -- and the unmapped-mode placeholder also isn't in the list. Per a minimum-change approach, revert the effect_list expansion and defer wiring extended effects into the generic effect_list/set_effect path to a follow-up PR (TODO added). Extended effects remain reported via `effect` and settable via the dedicated setExtendedCustomEffect API; discovery stays on `extended_custom_effect_pattern_list`.
|
Addressed in
On Separately, the earlier "new ValueError guards lack boundary tests" note was a visibility artifact (the test files weren't in the diff) — those boundary tests already exist ( |
Confirmed resolved at this head. Enum coercion narrowed to specific types — mismatched enum now raises. On Note: protocol.py and tests not in working tree, so golden-packet bytes unverified here. |
PR Review — feat: extended custom effects, segment colors, and scribble (per-LED) for 0xB6Solid, well-validated 0xB6 lighting API. Prior blocking findings are resolved; one documented, non-blocking Specific strengths:
Needs attention:
Unverified: 🟡 Important
1. effect_list omits the extended custom effects that `effect` now reports (0xB6)
|
| # Convert Enum to value if needed | ||
| if hasattr(pattern_id, "value"): | ||
| pattern_id = pattern_id.value | ||
| if hasattr(option, "value"): | ||
| option = option.value | ||
| if hasattr(direction, "value"): | ||
| direction = direction.value |
There was a problem hiding this comment.
hasattr is a bit of a codesmell here
bdraco
left a comment
There was a problem hiding this comment.
Please split this one up. We are back in the too big for human review space again.
Agree. I can break it down to one feature per PR. I will defer the |
Adds 0xB6's lighting capability on top of the merged protocol PR (#539): ~2,425 lines across 8 files (~1,550 of which are tests).
This is the bulk of the device's lighting capability. Three command families, all wrapped in the standard
B0 B1 B2 B3envelope withversion=0x02(reusing the existingconstruct_wrapped_message), all reverse-engineered and verified on hardware.1) Extended custom effects —
E1 21Whole-strip dynamic effects. Inner layout:
EXTENDED_CUSTOM_EFFECT_ID_NAME,pattern.py) was verified against the app's own labels across a 70-command capture (every id 1–22/101/102, plus direction/density/speed and all three option variants, decoded and matched).2) Segment colors —
E1 22Static colors for up to 20 fixed segments (each ≈ N/20 LEDs). Inner:
e1 22 00 00 00 00 14 [H/2 S V 00 00] × 20. Fewer than 20 supplied → remaining segments off; no auto-repeat (caller supplies up to 20). Verified on-device.3) Scribble (per-LED) —
E1 23+E1 26The per-bulb "scribble" feature. Verified on hardware:
E1 26is the render path; the device keeps a stateful per-LED framebuffer.E1 26: e1 26 EFFECT DIR DENSITY SPEED BLINK H/2 S V 00 WLVL BLINKSPD <bitmap ceil(N/8)B>
0 1 2 3 4 5 6 7 8 9 10 11 12 13 …
BLINK:0x00none /0x08slow /0x10fast; color brightness = V, white brightness = WLVL (mutually exclusive); off = paint(0,0,0). Bitmap is MSB-first, bit set = "apply this command to LED i".EFFECT(byte 2) is the scribble effect id; device accepts 0x00–0x08 (9 effects; named: 0 static, 1 flowing, 2 twinkling, 5 stars-wink, 8 accumulate — ids 3/4/6/7 are valid but unnamed by the app).E1 23(e1 23 01 00 01 50 64 00 N+ N×7B records) is a non-rendering mode-init the app sends on entry; the library sends it (all-zero) only to guarantee scribble-mode entry.Public API & code
const.py(+97)ExtendedCustomEffectPattern,ExtendedCustomEffectDirection,ExtendedCustomEffectOption,ScribbleEffect,ScribbleBlinkMode;ScribbleLEDNamedTuple (rgb | white, blink mode/speed).protocol.py(+326)construct_extended_custom_effect,construct_custom_segment_colors,construct_scribble_init,construct_scribble_paint,_scribble_bitmap, HSV helpers — all reuseconstruct_wrapped_message(..., version=0x02).base_device.py(+239)_generate_*validation +_scribble_paint_groups(the shared render path: groups LEDs by identical color/blink, oneE1 26per group covering all N incl. an off group; coerces+validates the effect id 0–8);supports_scribble.aiodevice.py(+82) /device.py(+96)async_set_extended_custom_effect/setExtendedCustomEffect,async_set_custom_segment_colors/setCustomSegmentColors,async_set_scribble/setScribble(camelCase sync to match existing methods).effect: ScribbleEffect | int.pattern.py(+34)EXTENDED_CUSTOM_EFFECT_ID_NAME(verified).Design decisions
Reuse the wrapper/HSV helpers rather than reinventing checksum/framing.
Grouped
E1 26is the render path (notE1 23), because on-device testing showedE1 23alone does not light the strip. A full per-LED config = oneE1 26per color/blink group, covering all N LEDs deterministically.effectaccepts a raw int (not just the named enum) so the 4 valid-but-unnamed device effects (ids 3/4/6/7) are reachable without fabricating names; the 0–8 range is validated centrally in_scribble_paint_groups.Public methods guard on capability —
set*/async_set_*raiseValueErroron devices lacking the feature (supports_extended_custom_effects/supports_scribble), rather than failing deep in construction (previously a bareAssertionError, or anAttributeErrorunderpython -O).Per-effect capability map (reverse-engineered from the vendor app)
The library enforces only general wire-byte bounds (
speed/density0–100,direction0x01/0x02,option0–2). Per-effect applicability below is documentedfor reference; it is intentionally not enforced (deferred to a future PR).
directionis not surfaced per-effect in the app;densityis a 0–100 byte(some effects expose it as discrete steps, e.g. Meteor low/middle/high).
optionvalues are 0-based indices into each effect's option list.NOTE Per-effect validation will add complexity to the code and CLI. The device still function on invalid param per effect. Not a hard block. Can be added in the future in a separate PR if needed.
Testing
E1 23init, etc.). Plus grouping, validation, and async/sync sequence tests.pytestgreen,ruffclean, no newmypyerrors.On-device verification (device
192.168.1.78, 80 LEDs):-c red/-c "#0000FF"/-c 0,255,0-w 80-c red -v-d 80/-k 3500 80-c-c 255,0,0,128-w-C wave …,-C meteor …-C segments 0 "<≤20 colors>"-C scribble static "…"40xblue(pad) /100xred(truncate)Brightness/PWM note: at reduced brightness the device shows a sub-visual PWM flicker visible only on a security camera. Confirmed device/hardware behavior — the official app flickers identically at low brightness; full-brightness output is steady. Not introduced by this code (the library faithfully sends the requested brightness).
Review round: hardware-verified state & color-set fidelity
Follow-up to review feedback, resolved by capturing the vendor app's traffic on the physical device (192.168.1.78) and matching it byte-for-byte. Established the device's two top-level modes from real
EA 81state frames:preset_pattern=0x24→ a plain color (set via uniformE1 22).preset_pattern=0x25,mode= pattern id → an effect (set viaE1 21).Fixes (all hardware-verified):
preset 0x24now maps toeffect=Nonewith the real brightness, instead of the phantom name"Segments"/brightness=255. Dropped the unreachable0x00:"Segments"and0x6E:"Solid Color"table entries (the app never emits either — confirmed by capture).setRgb/setWarmWhitenow match the app. They send a uniformE1 22(Colorful →preset 0x24), so a plain color-set round-trips as a color — previously they sentE1 21Static Fill, landing inpreset 0x25and reporting as the effect "Static Fill".setWarmWhite(50%)shipped full brightness); now combined bymax. Verified linear: 20/50/100% → device white 20/49/100.E1 21header byte fidelity: byte[2]0x00→0x01, matching every committed app command.speed/density0–100,direction ∈ {0x01,0x02},option0–2, pattern id 1–22/101/102; publicset*methods raiseValueErroron unsupported devices.Unit tests for these paths are anchored to the real captured device packets (exact
EA 81state frames and the app'sE1 22"Colorful → red" command).Note: CCT devices are unaffected — the color/white changes live in the 0xB6-only
ProtocolLEDENETExtendedCustomoverride; CCT/multi-white devices use their own protocol classes (warm/cool sent as separate bytes) and are byte-for-byte unchanged.Follow-up polish (review round 2):
ExtendedCustomEffectPattern/Direction/Optionenums are now accepted bysetExtendedCustomEffect/async_set_extended_custom_effect(coerced to.valuebefore validation), consistent withsetScribble.E1 23init, so an invalid call raises without leaving the device in a half-applied init state.preset 0x25scene mode now reports"unknown (0x…)"instead ofNone, so an active-but-unknown effect isn't conflated with idle.Follow-up polish (review round 3):
E1 23init send; unmapped0x25scene modes report"unknown (0x…)"instead ofNone;extended_custom_effect_pattern_listis Title-Case aligned witheffect; non-blinking scribble LEDs coalesce into oneE1 26; effect-enum coercion is narrowed to the specific enum types.effectand set via the dedicatedsetExtendedCustomEffectAPI (discovery viaextended_custom_effect_pattern_list). Wiring them into Home Assistant's genericeffect_list/set_effectis intentionally deferred to a follow-up PR — that path takes only(name, speed)and cannot supply the color list these effects require. Consequently a running scene may report aneffectname not present ineffect_list; this is a documented, intentional limitation of this first-introduction PR, not an oversight.