Repository navigation
feat(lighting): auto-detect pre-09W29 F414 coarse-only dimmers via speed 240 (0xF0) and emit WHAT 2..10 presets - #134
Open
GreenGrassBlueOcean wants to merge 4 commits into
Open
GreenGrassBlueOcean wants to merge 4 commits into
GreenGrassBlueOcean wants to merge 4 commits into
Conversation
…eed 240 (0xF0) and emit WHAT 2..10 presets
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… command brightness accessors
…mmand.is_coarse_dimmer
fedem95
approved these changes
Oct 22, 2026
fedem95
left a comment
Contributor
There was a problem hiding this comment.
Looks good. The legacy dimmer detection, per-gateway isolation, and coarse-to-fine brightness mapping are covered, and all CI checks are passing. Approved.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the problem
In
OpenWebNet-HA/MyHOME#767,@wave68runnerreported that dimming a BTicinoF414modular dimmer (WHERE=61behind anMH200Ngateway, firmware1.0) from the Home Assistant UI did not change the live lamp output, whereas switching the dimmer ON/OFF and dimming from the iMyHome app worked normally:F414commercial catalog reference (cut-over around batch09W29):F414(09W29and later, e.g.WHERE=99inMyHOME#466) and modern dimmers (F418U2,F416U1,F417U2): Report a real transition speed (2at rest,5while actively ramping) in the second slot ofWHO=1DIMENSION 1(*#1*99*1*150*5##) and execute extended 8-byteDIMENSION 1writes (*#1*WHERE*#1*<101..200>*<SPEED>##) immediately on the live triac output.F414firmware (pre-09W29, e.g.WHERE=61inMyHOME#767): Always reports240(0xF0on the SCS bus, indicating an unimplemented transition-speed byte) in the second slot ofDIMENSION 1(*#1*61*1*130*240##).F414behavior onDIMENSION 1writes vs coarseWHAT 2..10commands:DIMENSION 1writes withSPEED = 0(*#1*61*#1*150*0##, whatOWNLightingCommand.set_brightness("61", 50, 0)previously emitted) are ignored completely by the actuator.DIMENSION 1writes withSPEED != 0(*#1*61*#1*150*1##or*#1*61*#1*150*255##) do not dim the live output while the lamp is ON — they only write150into the dimmer's stored turn-on preset register (taking effect only after an OFF*1*0*61##*1*1*61##cycle).*1*2*61##…*1*10*61##) actuate the live lamp immediately.WHAT 2..10vs fineLEVEL100(101..200) percentage mismatch:OWNLightingEventleftbrightness = Noneon coarse preset events (*1*2..10*WHERE##), causing downstream consumers to approximatebrightness_preset * 10(WHAT 5 -> 50%,WHAT 7 -> 70%) even though the physical dimmer and BTicino'slibqtdevices(TS10_1_0_23) mapWHAT 2..10non-linearly (2->1%, 3->10%, 4->20%, 5->30%, 6->40%, 7->50%, 8->60%, 9->75%, 10->100%).How is it solved
OWNd/message/lighting.py,OWNd/message/base.py,OWNd/connection/event_session.py,OWNd/connection/command_session.py,OWNd/transport/serial.py,OWNd/message/__init__.py):DIMMER_SPEED_COARSE_ONLY = 240(0xF0).is_coarse_dimmer_where(where, *, gateway=None),register_coarse_dimmer_where(where, is_coarse=True, *, gateway=None), andclear_coarse_dimmer_wheres(gateway=None)backed by_COARSE_DIMMER_BY_GATEWAY: dict[tuple[str | None, str], bool]keyed by(gateway_key, normalize_where(where)):gatewayacceptsNone, a gateway string key (MAC/serial, host, or serial port), or anOWNGatewayobject (.serial/.unique_id/.host/.address).OWNMessage.parse(data, *, gateway=None),OWNEvent.parse(data, *, gateway=None), andOWNCommand.parse(data, *, gateway=None)bind the active gateway via aContextVar(_PARSE_GATEWAY_CTX), andOWNEventSession.get_next(),OWNCommandSession._read_command_response(), andAsyncSerialTransport._process_frame()pass their gateway/port identifier automatically.09W29F414atWHERE=61(SPEED = 240) and Plant B has a modernF418U2atWHERE=61(SPEED = 2), Plant B'sDIMENSION 1replies (("gw_b", "61") = False) never overwrite or clear Plant A's("gw_a", "61") = Trueentry; scoped lookups (gateway=gw_avsgateway=gw_b) remain strictly isolated, while unscoped single-plant callers (gateway=None) resolveTruewhen all plants that have reportedWHEREagree it is coarse (bool(scoped) and all(scoped)).F414atWHERE=61on a given gateway is replaced by anF418U2or post-09W29F414(SPEED = 2/5), its entry on that gateway is automatically updated toFalse.libqtdevices(TS10_1_0_23) CoarseOWNd/message/lighting.py):COARSE_PRESET_TO_PERCENT = {2: 1, 3: 10, 4: 20, 5: 30, 6: 40, 7: 50, 8: 60, 9: 75, 10: 100}andbrightness_preset_to_percent(preset)(dimmerLevelTo100inlighting_device.cpp).OWNLightingEvent.brightness_preset_percent(int | None), and when a coarse preset event (*1*2..10*WHERE##) is parsed for a learned coarse-only dimmer address, populatedevent.brightnesswithbrightness_preset_to_percent(self._state)soMyHOMEmaintains a consistent 1–100 % state without jumping betweenpreset * 10(70%) andDIMENSION 1(50%).percent_to_brightness_preset(level)implementinglibqtdevicesdimmer100LevelTo10(lighting_device.cpp:240-245:<=0 -> 0,1..4 -> 2,5..15 -> 3,16..25 -> 4,26..35 -> 5,36..45 -> 6,46..53 -> 7,54..67 -> 8,68..85 -> 9,>=86 -> 10).brightness_preset_to_percent(),percent_to_brightness_preset(),OWNLightingCommand.set_brightness_preset(), andOWNLightingCommand.set_brightness()to rejectbooland non-integral/non-finitefloats (50.9,7.9,NaN,±inf) withValueErrorrather than silently truncating viaint().OWNLightingCommand(OWNd/message/lighting.py):OWNLightingCommand.set_brightness(where, _level=30, _transition=0, *, coarse: bool | None = None, gateway: Any = None):use_coarse = is_coarse_dimmer_where(where, gateway=gateway) if coarse is None else bool(coarse).use_coarseisTrue, emits*1*0*WHERE##for_level <= 0(ignoring transition speed perDimmerDevice::setLevel100) and delegates1..100%tocls.set_brightness_preset(where, percent_to_brightness_preset(int(_level)))(*1*2..10*WHERE##).OWNLightingCommand.is_coarse_dimmer(bool),OWNLightingCommand.brightness_preset(int | None), and extendedOWNLightingCommand.brightnessto decode coarseWHAT 2..10commands (brightness_preset_to_percent(self._what)) when built viaset_brightness(..., coarse=True)or targeting a learned coarse dimmer, keeping command and event accessors symmetric.README.md,CHANGELOG.md,scripts/sync_documentation.py):WHO=1catalog description inREADME.md/scripts/sync_documentation.pyand added## [Unreleased]notes inCHANGELOG.md.How is it tested
tests/test_who1_ts10_builders.py:test_coarse_dimmer_conversion_and_detection: verifiesbrightness_preset_to_percent()andpercent_to_brightness_preset()across all threshold boundaries and invalidbool, non-integralfloat(7.9,50.9), and non-finitefloat(NaN,±inf) inputs; verifies automatic learning from*#1*61*1*130*240##, coarse command generation (*1*7*61##,*1*9*61##,*1*0*61##),OWNLightingCommand.is_coarse_dimmer/brightness/brightness_presetsymmetry, explicitcoarse=True/coarse=Falseoverrides, private riser (61#4#01) isolation, automatic upgrade back toDIMENSION 1(*#1*61*#1*150*0##) when*#1*61*1*200*2##is received, and multi-gateway isolation when twoOWNGatewayinstances (gw_awith coarseF414at61andgw_bwith fineF418U2at61) shareWHERE=61in the same process.tests/golden/frames/who01_lighting.yaml&tests/golden/corpus.json:light.dim.reply.level.30.speed240.61(*#1*61*1*130*240##) andlight.cmd.preset.7.61(*1*7*61##).MyHOMETrace Ingestion & Replay (OpenWebNet-HA/MyHOME#776):@wave68runner's full 43-frame capture intoMyHOME/tests/fixtures/traces/issue_767/myhome_trace_MH200N_all_2026-10-21T18-12-44.jsonand replays all 43 frames end-to-end inMyHOME/tests/test_issue_767_mh200n_f414_trace.py.OWNd&MyHOMESuites:OWNd/message/lighting.pyat 100.0% statement and branch coverage (607 Stmts, 0 Miss, 200 Branch, 0 BrPart),1902 passedacross the fullOWNdsuite,mypy OWNd tests/typing(0 issues),ruff checkclean,scripts/verify_library_standards.pyandscripts/sync_documentation.py --checkpassing, andMyHOMElighting + trace test suites (353 passed) green.State Diagram
stateDiagram-v2 direction TB state "1. Per-Gateway Dimmer Capability Learning (OWNLightingEvent)" as Learning { [*] --> UnlearnedWhere : Default ((gw_key, where) not in _COARSE_DIMMER_BY_GATEWAY) UnlearnedWhere --> CoarseDimmer : Rx *#1*WHERE*1*LEVEL100*240##\n(transition == 240 / 0xF0)\nregister_coarse_dimmer_where(where, True, gateway=gw) UnlearnedWhere --> FineDimmer : Rx *#1*WHERE*1*LEVEL100*2..5##\n(transition != 240)\nregister_coarse_dimmer_where(where, False, gateway=gw) CoarseDimmer --> FineDimmer : Hardware replaced at WHERE -> Rx *#1*WHERE*1*LEVEL100*2##\n(transition != 240 sets (gw_key, where) = False) FineDimmer --> CoarseDimmer : Rx *#1*WHERE*1*LEVEL100*240## } state "2. Brightness Command Dispatch (OWNLightingCommand.set_brightness)" as Dispatch { [*] --> CheckLevel : set_brightness(where, level, transition, coarse=None, gateway=None) CheckLevel --> ResolveMode : use_coarse = is_coarse_dimmer_where(where, gateway=gateway) if coarse is None else coarse ResolveMode --> CoarseOff : use_coarse=True & level <= 0\n-> *1*0*WHERE## ResolveMode --> CoarsePreset : use_coarse=True & level 1..100\npercent_to_brightness_preset(level)\n-> *1*2..10*WHERE## ResolveMode --> FineOff : use_coarse=False & level <= 0\n-> *1*0*WHERE## or *1*0#SPEED*WHERE## ResolveMode --> FineDim1 : use_coarse=False & level 1..100\n-> *#1*WHERE*#1*(100+level)*SPEED## CoarseOff --> [*] CoarsePreset --> [*] FineOff --> [*] FineDim1 --> [*] }Where are the traces
WHO=1Legacy Pre-09W29BTicino F414Dimmer (SPEED=240)MyHOME/tests/fixtures/traces/issue_767/myhome_trace_MH200N_all_2026-10-21T18-12-44.json(OpenWebNet-HA/MyHOME#776/MyHOME#767) &tests/golden/frames/who01_lighting.yaml@wave68runner(MyHOME#767) — BTicinoMH200N(FW1.0,WHO=13code44) + legacyBTicino F414dimmer (WHERE 61)*1*1000#30*61##*1*10*61##,*1*1000#31*61##*1*9*61##,*1*8*61##,*1*7*61##,*1*6*61##,*1*5*61##(frames[16..24]),*1*1*61##*1*5*61##(frames[12..13]),*#1*61*1##*#1*61*1*130*240##(30%,SPEED=240,frames[14..15]),*#1*61*1*178*240##,*#1*61*1*146*240##,*#1*61*1*181*240##,*#1*61*1*140*240##(frames[35..41])WHO=1Post-09W29BTicino F414(SPEED=2/5) &F418U2ComparisonMyHOME/tests/fixtures/traces/issue_466/myhome_trace_MH200_f414_dimmer_2026-09-26T21-59-00.json&myhome_trace_MH200_f418u2_dimmer_2026-09-27T12-15-00.json(OpenWebNet-HA/MyHOME#466)@GreenGrassBlueOcean(MyHOME#466) — BTicinoMH200(FW2.1.0) +BTicino F414(WHERE 99) &BTicino F418U2(WHERE 62)*#1*99*1##*#1*99*1*200*2##,*1*9*99##*#1*99*1*174*2##,*1*8*99##*#1*99*1*163*2##,*#1*99*#1*150*0##*#1*99*1*150*5##*1*7*99##What does the Oracle / Encyclopedia say
functional/who-1-lighting/what.md,functional/who-1-lighting/dimensions.md,devices/definitions/own-dev-0025-din-dimmer-1000-w.md):libqtdevicesTS10_1_0_23,lighting_device.cpp:240-245):DimmerDevice(coarse 10-step dimmer) convertsWHAT 2..10to cached fine levels1, 10, 20, 30, 40, 50, 60, 75, 100(dimmerLevelTo100), matching the empiricalDIMENSION 1readbacks on both@wave68runner'sF414(WHAT 5 -> 130,WHAT 7 -> 150) and@GreenGrassBlueOcean'sF414/F418U2(WHAT 3 -> 110,WHAT 5 -> 130,WHAT 7 -> 150,WHAT 8 -> 163,WHAT 9 -> 174,WHAT 10 -> 200).dimmer100LevelTo10) ignores the transition speed argument and maps requested fine percentages0..100toWHAT 0(0%) andWHAT 2..10(1..4 -> 2,5..15 -> 3,16..25 -> 4,26..35 -> 5,36..45 -> 6,46..53 -> 7,54..67 -> 8,68..85 -> 9,86..100 -> 10).bt_luciDisassembly (own-dev-0025-din-dimmer-1000-w.md,functional/who-1-lighting/dimensions.md):F414(DIN dimmer 1000 W) underwent a hardware/firmware revision around batch09W29; pre-09W29units fill theSPEEDbyte of the extended SCS0x0Dstatus telegram (18 B8 <WHERE> 01 44 0D 0D <LEVEL_HEX> <SPEED_HEX>) with0xF0(240in*#1*WHERE*1*LEVEL100*SPEED##) and only actuate live output on 4-byteWHAT 2..10($03<WW>0012<WHAT-1>D) /30/31commands.tests/golden/firmware_oracle.json):F453010100,F453AV030014,F454020051,F455020022,F459020105,F460010023,F461010104,H4684020054,MH200020102,MH200N010108,MyHomeServer1028206), both coarse preset commands (*1*3*31##) and fineDIMENSION 1write commands (*#1*31*#1*101*0##,*#1*31*#1*150*5##,*#1*31*#1*200*0##) produceverdict: "out", reply: "ack", confirming that the IP gateway forwards both forms onto the SCS bus and that the distinction between live coarse actuation andDIMENSION 1actuation resides in the downstreamF414actuator firmware (SPEED = 240vsSPEED = 2/5).