Repository navigation
feat(cover,light): pluggable CoverKinematicsStrategy hierarchy and low-priority fade step routing (#730) - #739
Conversation
7775849 to
9565bcd
Compare
6def5ca to
d919be7
Compare
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
c630e76 to
37df2a8
Compare
4da855e to
c8b0378
Compare
|
Not a review, just a note from the calibration side, since this is where the cover model is being given its shape. Please leave room for a calibrated, non-linear strategy. The guided calibration in xtimmy86x#10 (coming to this repo through #374) produces per-cover geometry: slat phase, roll coefficients, lift-off gap and a reference travel, measured with a tape. Time and height are not proportional on these shutters: from a full close, a 40 % request on my 145 cm shutter lifts the curtain about 18 % (the first seconds open the slats), and @fedem95's 40 cm / 55 cm asymmetry on the same 50 % is the same effect seen from the other side. A linear Two things to align before review:
|
5f30c58 to
bda0c54
Compare
bda0c54 to
2be0103
Compare
2be0103 to
587efb1
Compare
|
Thanks @Interstellar0verdrive — we re-cut the branch on top of the current 1. Strategy owns
|
587efb1 to
58ffd33
Compare
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Audit of 58ffd339. CI is green and the branch merges cleanly. The refactor mostly preserves existing behaviour: timed covers, the Venetian slat lead time, min_motor_pulse, stop-echo re-anchoring and the fade-step routing all match the old code. The new strategy: key and travel_time: 0 introduce three bugs, each reproduced with a throwaway test against this head.
Bugs
- Contradictory config is accepted.
strategy: timed|simple|venetiantogether withadvanced: truepassescover_schema.resolve_cover_strategyhonoursstrategy, butcover.pystill takes the advanced paths fromself._advanced.timed+advanced: Dimension 11 writes lose the #702 deadband (quantize_target(50, 51)returns 51, not 52).simple+advanced: the Dimension 10 level is stored (40 in the probe) butcurrent_cover_positionreturnsNone, and SET_POSITION is gone.- The
_advancedguard inTimedCurtainStrategy.current_positiononly exists to paper over this.
- A simple cover never reports closed. After a bus
*2*2*21##and*2*0*21##,is_closedstaysFalseand the position staysNone, so HA shows open permanently. reset_cover_travel_timedoes not undo the promotion. Atravel_time: 0cover that was promoted on calibration staystimedwith 0.0 s travel after a reset: SET_POSITION is still advertised,duration_forreturns 0.0 andposition_afternever moves.
Description and comment claims
- The F46x NACKs in the oracle table are an emulator artefact. The oracle data is reproduced accurately, but the oracle also has F460/F461/MyHomeServer1 NACK a plain
*2*1*31##, which the live F461 trace in this PR shows working.tests/fixtures/traces/f461_live_2026-10-18/README.mdexplains why: a physical F461 NACKs when no actuator answers, and it ACKed*#1*08*#1*150*0##in 177 ms. "F46x NACK Dimension 1 transitions" is therefore not evidence. Keepingsoftware_steppedas the default is still right because of the F450 and legacy dimmers. - Trace #3 is synthetic.
oracle_who2_venetian_tilt/README.mdsays it is fully synthetic, derived from the client model, so the replay is circular. It should not be presented as a firmware-oracle trace. - "Drop-oldest on queue saturation" does not exist. Fade steps are only dropped when superseded by a newer frame for the same target (
drop_superseded). The final step of a fade also travels in the fade tier. - The cited test does not exist.
test_calibrated_geometry_strategy_plugs_in_without_cover_py_changesis not in the PR. The test that exists overrides onlytravel_for_direction(still linear) and calls the strategy directly, not through the entity. - Some listed removals are not in the diff against the base.
ImpulseDoorStrategy,tilt_time,TRANSITION_MODE_OFFandallow_multi_workerwere removed from an earlier iteration of this branch, not fromv2-phase1-architecture.
Nits
- A simple cover still publishes
travel_time: 25/calibration_source: defaultwhile_travel_for()returns 0. - The strategies read and write entity private attributes through
entity: Any;reanchor_on_stop_echowrites five of them plus_last_run. mypy cannot check those. strategyandtravel_time: 0are not documented indocs/configuration/covers.md.
Fixes for the three bugs, the docs and the description follow on this branch.
| where = cfg.get(CONF_WHERE) | ||
| if not where or not str(where).startswith("#"): | ||
| raise Invalid("Members can only be defined on a group cover (where must start with #)") | ||
| strategy = cfg.get(CONF_STRATEGY) |
There was a problem hiding this comment.
Only simple + slat_tilt is rejected here. strategy: timed|simple|venetian with advanced: true, strategy: timed with slat_tilt (silently becomes venetian through _enable_slat_tilt), and strategy: timed with travel_time: 0 all pass.
| slat_time=raw_slat_time, | ||
| min_motor_pulse=raw_min_pulse, | ||
| ) | ||
| if isinstance(self._strategy, NativePositionStrategy): |
There was a problem hiding this comment.
This only ever turns _advanced on. With an explicit non-native strategy and advanced: true, the entity keeps the advanced code paths while the strategy is timed or simple. Deriving _advanced (and _slat_tilt) from the resolved strategy keeps the two consistent.
| ) | ||
| return result | ||
|
|
||
| def _promote_to_timed_strategy(self) -> None: |
There was a problem hiding this comment.
Nothing reverses this. async_reset_travel_time (unchanged, further down) writes the YAML travel_time: 0 as 0.0 s into the promoted TimedCurtainStrategy and leaves SET_POSITION advertised. The cover should drop back to SimpleUpDownStopStrategy here.
|
|
||
| def current_position(self, entity: Any, now: float) -> int | None: | ||
| """Interpolate current position from start position, motor anchor, and strategy kinematics.""" | ||
| if getattr(entity, "_advanced", False) is True: |
There was a problem hiding this comment.
A timed strategy on an advanced entity is the contradictory-config case. Once the entity flags follow the strategy, this guard is dead code.
…r closure, demote on reset (#739) - validate: an explicit strategy cannot be combined with advanced (other than native), strategy: timed with slat_tilt or with travel_time: 0. - cover: _advanced / _slat_tilt follow the resolved strategy, so the advanced code paths never run under a timed or simple strategy (the #702 deadband was lost and Dimension 10 levels were hidden); drop the matching guard in TimedCurtainStrategy.current_position. - cover: an assumed-state (simple) cover is closed after a closing run and open after an opening one instead of staying open. - cover: reset_cover_travel_time returns a promoted travel_time: 0 cover to SimpleUpDownStopStrategy; an explicit strategy: simple is never promoted, matching what a restart resolves. - cover: travel attributes come from the strategy, so a simple cover shows no travel time. - docs: document strategy and travel_time: 0 in covers.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_demote_to_simple_strategy's early return was never exercised, failing the 100% coverage gate. Drop the unreachable NativePositionStrategy check (reset returns before demotion on advanced covers) and test a reset on a simple cover that was never promoted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interstellar0verdrive
left a comment
There was a problem hiding this comment.
Thanks for re-cutting this. All three points from my note were taken up: the strategy now owns position_after / duration_for / reanchor_on_stop_echo, the #702/#707/#708 logic lives in the strategies, and slat_time is the single slat number. I checked behaviour preservation: my #508 probe set and the #702/#707/#708 cases (0.92 s / 56 %, the 0.4 s floor to 53 %, the settle at travel + 2 s, advanced 100 % as plain UP, floor 0 off, a floor-only call keeping the calibration) give identical numbers on the base and on 082f723. The fade-step tier can never sit ahead of a cover frame. As always, this is code review only.
What still stops a calibrated strategy from plugging in without touching cover.py:
cover.pyoverrides the lead. It computesleaditself (0 unlessslat_tilt) and passes it toposition_after, whileduration_foruses the strategy's own. With a 3 s slat phase on a cover without tilt control,set_position0→20 stops at the right time but reports 50, and a 3 s keypad run from closed reports 30. Let the strategy own the lead everywhere.- Strategies are rebuilt from fixed classes.
_enable_slat_tilt,_promote_to_timed_strategyand_demote_to_simple_strategyreplace a custom strategy with Timed/Venetian/Simple and drop its parameters. - There are no run lifecycle hooks. A stateful model needs: run started, run ended (stop / end stop / reversal / abort), position confirmed (end stop, tape reading), position unknown until a full run, and state that survives a restart. #374's tracker is built exactly around those calls; without them it would have to stay as a second path in
cover.py. - The test the comment names isn't in the PR. The existing one only overrides
travel_for_directionlinearly and callsduration_fordirectly. A piecewise strategy driven through set-position, a keypad run, the stop echo and the 0/100 settle would prove the contract.
With 1–4 in place I'd be glad to approve.
…#739) Addresses the #739 review: a calibrated strategy (#374) now plugs into MyHOMECover without touching cover.py. - Lead: every curtain run takes its lead from strategy.lead_time_for, not only covers with slat_tilt; the same lead drives the freeze, set-position and stop-echo paths. An auto-stop re-anchor clears it for the remaining segment (_move_lead), as a Venetian blind's turned slats did before. - Transitions: with_slat_tilt / promoted / demoted let the strategy pick its successor. Only the built-in classes swap, carrying their timing parameters exactly; any other strategy keeps itself. - Lifecycle: on_run_started, on_run_ended (stop, end_stop, takeover, reversal, abort) and on_position_confirmed (end stop, calibration); position_after may return None while the position is unknown; export_state / restore_state ride on the HA restore data, keyed by strategy name. Built-in strategies are stateless no-ops. - tests/test_cover_strategy_contract.py drives a piecewise, stateful strategy (3 s slat phase without tilt control) through keypad runs, set-position, the stop echo, the 0/100 settle, every run end, unknown position and a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Interstellar0verdrive thanks for the review. All four points are addressed in 5882b89:
Full suite 4,331 passed, 100% coverage on every module, mypy ratchet at 0. |
Interstellar0verdrive
left a comment
There was a problem hiding this comment.
Approving 5882b89. Same caveat as always: code review only, plus my own probes.
All four points are in:
- The lead comes from the strategy on every run. With a 3 s slat phase on a cover without tilt control, a 3 s keypad run from closed stays at 0 and
set_position0→20 reports 20. with_slat_tilt,promotedanddemotedkeep a custom strategy, same object and parameters.- The lifecycle hooks are called on every path: motor start; stop, takeover, reversal and abort; the 0/100 settle with a confirmed end stop; calibration; restore after a restart. The calls in #374's motion tracker map onto them one to one, so the calibrated model can come in as a strategy.
test_cover_strategy_contract.pydrives a piecewise, stateful strategy through the entity.
Behaviour is unchanged: my #508 probe set and the #702/#707/#708 cases give the same numbers as the base.
Two nits for later: a keypad run that hits the limit switch reports stop, not end_stop (the bus can't tell), and the scope cover's run timeout sets 100/0 without confirming the position.
Closes #730.
This PR extracts
MyHOMECoverkinematics into a pluggableCoverKinematicsStrategyhierarchy (custom_components/myhome/cover_strategies/) and routes intermediate software fade steps inMyHOMELightthroughgateway.send_fade_step(cmd)(FramePriority.LOW_FADE_STEP).What is the problem
A. Entangled Cover Kinematics in
MyHOMECover(custom_components/myhome/cover.py)BT-F411/2,F411/4,LN4672M2): Open-loop actuators that only emitWHAT=1(UP),WHAT=2(DOWN), andWHAT=0(STOP) on the bus. Position is estimated via dead-reckoning using direction-asymmetric travel times (travel_time_up,travel_time_down), a per-cover minimum motor pulse floor (min_motor_pulse, Covers: per-cover motor start-up floor instead of one 0.4 s constant (#466) #704 / feat(cover): per-cover motor start-up floor instead of single constant (#704) #708), and stop-status echo re-anchoring (Covers: end a timed run at the stop status, not the stop write (#466) #705 / fix(cover): end timed runs on stop status instead of stop write (#705) #707).slat_tilt: true, feat(cover): support venetian blind slat / louvre tilt positioning (WHO 2 Dimension 11) #492 / feat(cover): implement venetian blind slat tilt inching and clarify dimension 11 shutter level (#492) #508): End-to-endtravel_timeincludes one full slat rotation (slat_time). Moving the curtain requires subtracting slat lead time (curtain_travel = travel_time - slat_time), carrying pulse overshoot past 100% slat rotation into vertical curtain displacement, and re-anchoring both curtain position and slat angle on stop echoes.H4661M2,LN4661M2,AM5861M2,067557,F430—OWN-DEV-0045/ Object218): Report authoritative bus position via Dimension 10 (*#2*WHERE*10*STATUS*LEVEL*PRIORITY*INFO##) and accept absolute target writes via Dimension 11 (*#2*WHERE*#11#001*LEVEL##), requiring sub-deadband quantization (MIN_POSITION_DELTA = 2, Community Call for Real-World Gateway Traces: Help Us Eliminate Hardware Blind Spots! 📡 #466 / feat(cover): motor start-up floor and full runs at 0/100% (#466) #702) and zero local stopwatch timers.travel_time: 0orstrategy: simple): Standard relays where no travel time has been measured. Fabricating a50%startup position and exposing a percentage slider misrepresents bus state.entity._travel_for, a futureCalibratedGeometryStrategy(modeling non-linear slat phase + roll diameter + lift-off gap) cannot replace the timecover.pyagain.B. Software Fade Step Starvation vs. Gateway Firmware NACKs (
custom_components/myhome/light.py)SoftwareFadeEnginetransition,MyHOMELight._set_brightness_instantdispatches up to 25 intermediate*#1*WHERE*#1*LEVEL100*0##frames at0.3 sintervals. Sending those intermediate steps throughgateway.send(cmd)(FramePriority.HIGH_INTERACTIVE) places them at the same priority as interactive wall-switch commands and time-critical coverSTOPcommands (*2*0*WHERE##).DEFAULT_TRANSITION_MODEcannot blindly switch to"native": The firmware oracle shows theF450dropping Dimension 1 transition-speed writes (*#1*WHERE*#1*LEVEL100*SPEED##), and legacy dimmers ignore the hardware speed byte. The oracle also hasF460,F461andMyHomeServer1NACK them, but that NACK is an emulator artefact (see the note under the oracle table).How is it solved
A. Pluggable
CoverKinematicsStrategyProtocol & Hierarchy (custom_components/myhome/cover_strategies/)base.py):CoverKinematicsStrategy(@runtime_checkable Protocol) andBaseCoverStrategydirectly owntravel_time_down,travel_time_up,min_motor_pulse, andslat_time(never reading back fromentity._travel_for,entity._slat_time, orentity._min_motor_pulse).MyHOMECoverexposes_travel_time_down,_travel_time_up,_min_motor_pulse, and_slat_timeas@propertygetters/setters backed byself._strategy, guaranteeing that calibration updates and futureCalibratedGeometryStrategysubclasses (feat: add dedicated MyHOME administration panel #374) are never bypassed.position_after(start, elapsed, direction, *, entity=None, lead=None) -> int | Noneduration_for(start, target, direction=None, *, entity=None) -> floatreanchor_on_stop_echo(entity, ctx, now) -> int | Nonecurrent_position(entity, now) -> int | Nonecurtain_travel_for(opening) -> float&lead_time_for(opening, *, entity=None) -> floatturn_slats_after(start_tilt, elapsed, direction) -> int,carry_overshoot_into_curtain(...) -> int,tilt_pulse_duration_for(...) -> floatquantize_target(current, target) -> intresolve_cover_strategy()(__init__.py):validate.py/resolve_cover_strategy)supported_featuresassumed_stateSimpleUpDownStopStrategy(simple.py)strategy: simpleortravel_time: 0/NoneOPEN | CLOSE | STOPTruecurrent_cover_position = None(no fake50%). Auto-promotes toTimedCurtainStrategy/VenetianTiltStrategyvia_promote_to_timed_strategy()when calibrated.TimedCurtainStrategy(timed.py)strategy: timedortravel_time > 0(default25 s)OPEN | CLOSE | STOP | SET_POSITIONFalsetravel_time_up/travel_time_down,min_motor_pulsefloor (#704/#708), and stop-echo re-anchoring (#705/#707).VenetianTiltStrategy(venetian.py)strategy: venetianorslat_tilt: trueOPEN_TILT | CLOSE_TILT | STOP_TILT | SET_TILT_POSITIONFalseTimedCurtainStrategy. Shares singleslat_time(#508):curtain_travel = travel_time - slat_time, slat lead-time deduction, overshoot carry into curtain, and slat stop-echo re-anchoring.NativePositionStrategy(native.py)strategy: nativeoradvanced: trueOPEN | CLOSE | STOP | SET_POSITIONFalsequantize_target(MIN_POSITION_DELTA = 2, #466/#702) andduration_for = 0.0.validate.py,const.py):strategy: simple | timed | venetian | nativeandtravel_time: 0intocover_schemaand_validate_cover_members.ImpulseDoorStrategy/pulse_duration(avoiding overlap with feat(cover,lock): impulse garage/gate control, multi-factor safety approval, and intercom events #667MyHOMEImpulseCover), thetilt_timealias in favor of sharedslat_time(feat(cover): implement venetian blind slat tilt inching and clarify dimension 11 shutter level (#492) #508), and dead code (TRANSITION_MODE_OFF,allow_multi_worker, unreachable strategy lifecycle stubs).B. Low-Priority Software Fade Step Routing (
custom_components/myhome/light.py)DEFAULT_TRANSITION_MODE = TRANSITION_MODE_SOFTWARE("software_stepped").MyHOMELight._set_brightness_instant, whenself._fade_engine.is_fadingisTrueandgateway.send_fade_stepis an async coroutine function, routes intermediate fade frames throughawait gateway.send_fade_step(cmd)(FramePriority.LOW_FADE_STEP; a queued step is dropped only when a newer fade step or command for the same light supersedes it) while one-shot brightness commands continue usingawait gateway.send(cmd)(FramePriority.HIGH_INTERACTIVE).C. Review fixes (review)
strategynext toadvanced(other thannative),strategy: timedwithslat_tiltor withtravel_time: 0. The entity's_advanced/_slat_tiltnow follow the resolved strategy, so the advanced code paths never run under a timed or simple strategy; the matching guard inTimedCurtainStrategy.current_positionis gone.reset_cover_travel_timedemotes a promotedtravel_time: 0cover back toSimpleUpDownStopStrategy; an explicitstrategy: simpleis never promoted, matching what a restart resolves.travel_time: 25.strategyandtravel_time: 0are documented indocs/configuration/covers.md.How is it tested
pytest tests -q; the 784 cover / validation tests were rerun after the last edit), 100% statement coverage ofcover_strategies/andvalidate.py.mypy --strict(0errors viascripts/typing_ratchet.py),ruff check,scripts/verify_ha_standards.py, andscripts/sync_documentation.py --checkall green.State Diagram
Cover Strategy Resolution & Runtime Promotion Lifecycle
stateDiagram-v2 [*] --> ResolveStrategy : MyHOMECover.__init__() state ResolveStrategy <<choice>> ResolveStrategy --> NativePositionStrategy : strategy=="native" or advanced==True ResolveStrategy --> VenetianTiltStrategy : strategy=="venetian" or slat_tilt==True ResolveStrategy --> SimpleUpDownStopStrategy : strategy=="simple" or travel_time <= 0 ResolveStrategy --> TimedCurtainStrategy : strategy=="timed" or travel_time > 0 state SimpleUpDownStopStrategy { [*] --> UncalibratedIdle : position = None, assumed_state = True UncalibratedIdle --> MovingUp : *2*1*WHERE## / async_open_cover() UncalibratedIdle --> MovingDown : *2*2*WHERE## / async_close_cover() MovingUp --> UncalibratedIdle : *2*0*WHERE## / async_stop_cover() MovingDown --> UncalibratedIdle : *2*0*WHERE## / async_stop_cover() } SimpleUpDownStopStrategy --> TimedCurtainStrategy : _promote_to_timed_strategy() (calibration saved & _slat_tilt==False) SimpleUpDownStopStrategy --> VenetianTiltStrategy : _promote_to_timed_strategy() (calibration saved & _slat_tilt==True) TimedCurtainStrategy --> VenetianTiltStrategy : _enable_slat_tilt()Timed & Venetian Kinematics State Machine (
TimedCurtainStrategy/VenetianTiltStrategy)stateDiagram-v2 [*] --> Idle : _start_position = pos (0..100) Idle --> SlatLeadPhase : Move command / bus *2*1|2*WHERE## (slat_tilt=True, lead_time_for() > 0) Idle --> CurtainMoving : Move command / bus *2*1|2*WHERE## (lead_time_for() == 0) Idle --> TiltPulseRunning : async_set_cover_tilt_position() (tilt_pulse_duration_for() >= min_motor_pulse) state SlatLeadPhase { [*] --> RotatingSlats : turn_slats_after(start_tilt, elapsed, dir) RotatingSlats --> LeadComplete : elapsed >= lead_time_for() } SlatLeadPhase --> CurtainMoving : Slats fully open/closed (curtain_elapsed = elapsed - lead) SlatLeadPhase --> StopEchoWindow : Early *2*0*WHERE## before lead completes (curtain_position unchanged) state CurtainMoving { [*] --> DeadReckoning : current_position() -> position_after(start, elapsed, dir, lead) DeadReckoning --> AutoStopArmed : Intermediate target (0 < target < 100, duration_for()) } CurtainMoving --> StopEchoWindow : Auto-stop / async_stop_cover() sends *2*0*WHERE## (captures _pending_stop_context) CurtainMoving --> Idle : External *2*0*WHERE## -> _freeze_position() + _turn_slats_for_run() TiltPulseRunning --> Idle : Pulse auto-stop *2*0*WHERE## or carry_overshoot_into_curtain() state StopEchoWindow { [*] --> AwaitingBusStopEcho : ECHO_WINDOW (2.0 s) AwaitingBusStopEcho --> Reanchored : Relayed *2*0*WHERE## -> strategy.reanchor_on_stop_echo(ctx, now) } StopEchoWindow --> Idle : Position & slat tilt re-anchored to actual bus stop timestampWhere are the traces
All three trace fixtures are checked into
tests/fixtures/traces/and replayed end-to-end throughMyHOMECoverintests/test_cover_strategies.py:tests/test_cover_strategies.py)TimedCurtainStrategytests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json2.0.11, kernel5.10.35, WHO 1013 code134) driving physicalBT-F411/2shutter actuator atWHERE="05"(5.0 scalibrated travel time,run="run2_shutter_05_travel_and_reversal", 18 RX frames).test_replay_f461_live_shutter_05_trace_on_timed_strategy(L510–L565)0%(closed).1. Open pulse
*2*1*05##(t=22.720s)*2*0*05##(t=24.852s,43%.2. Open pulse
*2*1*05##(t=26.421s)*2*0*05##(t=28.014s,75%.3. Close reversal
*2*2*05##(t=28.119s)*2*0*05##(t=34.341s,0%(is_closed = True).NativePositionStrategytests/fixtures/traces/issue_466/config_entry-myhome_F454_step_commands.jsonbus_monitor.recent_framesfrom a production BTicino F454 (fw2.0, 42 covers) atWHERE="13"(#466).test_replay_issue_466_f454_trace_on_native_strategy(L568–L613)*#2*13*10*10*100*001*0##(t=1791365333.130619, Dimension 10 status:shutterStatus=10Stop,shutterLevel=100%,priority=001,info=0) followed by*2*0*13##(t=1791365333.177027)current_cover_position == 100,is_closed = False.VenetianTiltStrategytests/fixtures/traces/oracle_who2_venetian_tilt/myhome_trace_oracle_who2_venetian_tilt.jsonF454fw2.0.51,cosoautomation_tr/automation_stm+ BTicino Object218fw192OWN-DEV-0045slat timingSLATS_ROTATION_TIME_DOWN) atWHERE="31"(travel_time=22s,slat_time=2.0s).test_replay_oracle_who2_venetian_tilt_trace_on_venetian_strategy(L616–L678)position=0%,tilt=0%.1.
1.0 sopen pulse*2*1*31##(t=1792080001.0)*2*0*31##(t=1792080002.0): rotates slats0% -> 50%while curtain stays at0%(absorbed bylead_time_for).2.
1.0 sclose pulse*2*2*31##(t=1792080010.0)*2*0*31##(t=1792080011.0): rotates slats50% -> 0%while curtain remains0%.(Additional reference capture cited by the Encyclopedia for WHO=1 Dimension 1 level/speed behavior:
tests/fixtures/traces/issue_466/myhome_trace_MH200_f418u2_dimmer_2026-09-27T12-15-00.json, plus MyHomeServer1/LN4660M2 WHO=2 traces intests/fixtures/traces/issue_445/).What does the Oracle / Encyclopedia say
A. Empirical Firmware Oracle (
openwebnet-mcp/own-firmware-oracle, 12 QEMU Emulated Gateways)ACK (*#*1##)+OUT (Bus)NACK (*#*0##)+OUT (Bus)NACK (*#*0##)+SILENT (Drop)*#1*31*#1*150*5##LEVEL100=150/ 50%,SPEED=5)F453,F453AV,F454,F459,H4684,MH200,MH200N,MH202F460(020012),F461(020011),MyHomeServer1(028206)F450(020010)F450drops them;F460,F461,MyHomeServer1NACK the command session). KeepingDEFAULT_TRANSITION_MODE = "software_stepped"and demoting active fade steps toFramePriority.LOW_FADE_STEPpreserves transitions everywhere without starving interactive commands.*2*1*31##/*2*0*31##WHAT=1) / STOP (WHAT=0)F453,F453AV,F454,F459,H4684,MH200,MH200N,MH202F460,F461,MyHomeServer1(emit24 30 ... 0don SCS bus +*2*1000#1*31##/*2*1000#0*31##)F450(020010)SimpleUpDownStopStrategy,TimedCurtainStrategy, andVenetianTiltStrategy. Gateways also emit*2*1000#0*WHERE##translation frames alongside*2*0*WHERE##stop echoes, handled by stop-echo re-anchoring.*#2*31*10##F454(020051),F459(020105),MH202(010024)F460,F461,MyHomeServer1F450,F453,F453AV,H4684,MH200,MH200NF450,F453,F453AV,H4684,MH200,MH200N) NACKs and drops Dimension 10 queries.NativePositionStrategymust only be active whenadvanced: true/strategy: nativeor when a Dimension 10 frame is actually observed on the bus.*#2*31*#11#001*40##priority=001)F454(020051),F459(020105),MH202(010024)F460,F461,MyHomeServer1(emit*#2*31*#11#001#1*40##)F450,F453,F453AV,H4684,MH200,MH200NNativePositionStrategytarget writes (*#2*WHERE*#11#001*LEVEL##) are hardware-timed onF454/F459/MH202/F46x/MyHomeServer1(duration_for() == 0.0) and unsupported on legacyMH200/MH200N/F453/H4684.Note
The F46x NACK column is not evidence of missing support. The emulated
F460,F461andMyHomeServer1NACK every frame above, including a plain*2*1*31##that the live F461 trace (#1) shows working. A physical F461 NACKs when no actuator answers on the bus, and the emulator has none; the same F461 ACKed the Dimension 1 write*#1*08*#1*150*0##in 177 ms (f461_live_2026-10-18/README.md). Keepingsoftware_steppedas the default rests on theF450and on legacy dimmers.B. OpenWebNet Encyclopedia (
OpenWebNet-Encyclopedia)DIMENSION 1(functional/who-1-lighting/dimensions.md):*#1*WHERE*#1*LEVEL100*SPEED##whereLEVEL100is offset by 100 (100= OFF,101..199= 1%..99%,200= 100%) andSPEEDis0(last speed),1..254(explicit speed), or255(default speed).MH200fw2.1.0+F418U2demonstrate that the writtenSPEEDparameter is not echoed unchanged in the followingDIMENSION 1report (a write withSPEED=0immediately reports trailing value5, and a later read returns2), andDIMENSION 1/DIMENSION 4support varies by gateway/actuator path.WHAT&DIMENSION 10/11(functional/who-2-automation/what.md,dimensions.md,README.md):*2*WHAT*WHERE##(0= Stop,1= Up,2= Down) with command-translation wrapper*2*1000#INNER_WHAT*WHERE##.DIMENSION 10(*#2*WHERE*10*SHUTTER_STATUS*SHUTTER_LEVEL*SHUTTER_PRIORITY*SHUTTER_INFO##) reportsshutterStatus(10= Stop,11= Up,12= Down,13= Step-Up,14= Step-Down) andshutterLevel(0..100%,255= unknown).DIMENSION 11(*#2*WHERE*#11#SHUTTER_PRIORITY*SHUTTER_LEVEL##, e.g.*#2*WHERE*#11#001*LEVEL##) writes an absolute target position (0..100%;255is never a valid target).TS10_1_0_23) treat standard 3-stateWHO=2objects strictly asUP / DOWN / STOPwithout fabricating percentage positions when Dimension 10 is absent — matchingSimpleUpDownStopStrategy.OWN-DEV-0045(devices/definitions/own-dev-0045-shutter-actuator-bus.md— Catalogue Item1586, Object218fw192:H4661M2,LN4661M2,AM5861M2,067557):UP_SHUTTER_TIME_MINUTES/SECONDS,DOWN_SHUTTER_TIME_MINUTES/SECONDS), minimum pulse duration registers (STOP_PULSE_DURATION,UP_OR_DOWN_PULSE_DURATIONin0.1 sincrements), and Venetian slat rotation parameters (SHUTTER_TYPE = 3"Standard with slats",SLATS_ROTATION_TIME_DOWN_H/L,SLATS_ROTATION_TIME_MIDDLE_H/L,SLATS_ROTATION_STEP_NUMBER), directly backing the parameter model inBaseCoverStrategy,TimedCurtainStrategy, andVenetianTiltStrategy.