Repository navigation
feat(expose): add declarative HA-to-SCS bus expose engine and virtual actuator LED sync - #750
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… actuator LED sync
…and strict typing
…ions and simplify WHO=2 dedup key
… against infeasible inputs
… against edge inputs
…s, and custom action security model
f37b2ba to
e5d49c2
Compare
|
Plant check of the mirror frames on the F461 (2.0.11), each frame sent twice (as command and as status request), virtual addresses 08 / 25 / 50 / 51:
Two smaller observations from the same session: the gateway relays the virtual-cover translation burst twice ( |
…, and skip WHO 13 sync when periodic_send=0 (#750)
|
Thank you @gdluck — this second live F461 ( Here is what we updated in 1. Default to
|
…cript and SCRIPT_SCHEMA (#750)
…rop OWNd shims (#750) OWNd 2.0.0b11 (with #129) declares the expose injection flags as bool | None, where None means no capture or oracle verdict exists. Only F461Profile sets them. Expose read them with bool(), and the shim's hasattr() patch never fired on b11, so WHO 4 and WHO 18 bindings silently sent nothing on every gateway except F461. - Gate WHO 4 and WHO 18 on _injection_block_reason(): frames are built only when the flag is exactly True (WHO 4 also accepts supports_dimension_response_injection=True). False, None and an unknown profile each block the binding. - async_sync_all() logs one warning per binding and reason, so a disabled binding is named at startup and again only if the reason changes (for example after a model correction). - _effective_profile() reads the gateway's .profile only. - Delete _ensure_ownd_expose_helpers and the profile-flag patches in where_grammar: the manifest pins OWNd 2.0.0b11, which ships them. - Read binding.who/where directly in the error logs instead of getattr(..., "where"), which test_where_usage_guard rejects. - Widen _iter_static_service_calls to list[Any] so mypy accepts the isinstance guard on nested choose/parallel items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) Bring in #742 (sweep gating, myhome.yaml copy in entry options) and #744 (#732 modularization of config_flow, gateway and platforms). Conflicts: - validate.py: import list; keep both CONF_STEP_*_ACTION and CONF_STRATEGY. - legacy_yaml.py: keep the base's _load_live_yaml/_apply_yaml_platforms structure and re-apply the expose handling on top: a top-level `expose:` key counts as single-gateway config, the where-filling loop skips `expose`, and the validated expose list is collected into configured_expose (threaded through _load_live_yaml). - config_flow.py: take the base's split module and move ExposeSubentryFlowHandler into config_flow_expose.py, next to config_flow_options.py; config_flow.py keeps async_get_supported_subentry_types and re-exports the handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… b11 Since #765 the mypy ratchet checks against the pinned OWNd 2.0.0b11 types, and expose.py reported 15 errors: gateway.send() takes OWNCommand while the frame lists were list[OWNMessage], and cmd/confirm took the type of their first branch (OWNEnergyCommand) before being reassigned to gateway, lighting and automation commands. Every builder returns an OWNCommand subclass, so the lists and both variables are now typed OWNCommand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WHO=4 spec (KB c003147) gives 0 as all probes, 1..99 as a zone's master probe and 001..099 as all probes of zone ZZ; an individual probe is PZZ with P=1..8 and ZZ=01..99. OPEN.db (c001132) adds the external probe class N00, used by the 3455 radio probes with N=1..9. The expose validator accepted the 0ZZ group addresses and P=9, and rejected N00. A virtual probe now takes 100..900 or 101..899 (ZZ != 00), "0" is no longer rewritten to "000", and is_probe() goes back to int >= 100, so climate and sensor no longer read 000..099 as probes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Count and summarize every pull request merged since 2.0.0b15, grouped by subsystem, with the expose engine (#750), slat tilt (#508) and impulse covers (#667) as the headline features. - Covers: the start-up floor is the per-cover min_motor_pulse (#708), timed runs end at the stop status (#707) and full-travel keypad runs report the end (#760); drop the "lift only" and open follow-up lines, both shipped. - New behaviour-change section: sync_time refuses half-hour time zones (#764) and advanced: true on an F411 now warns (#762). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What is the problem
In a BTicino / Legrand MyHOME SCS installation, users want to project Home Assistant entities and the system clock onto the OpenWebNet bus as virtual sensors, meters, and bidirectional virtual actuators (
expose, paired with OpenWebNet-HA/OWNd#129, merged into OWNdmaster; requires OWNd ≥ 2.0.0b11, whichmanifest.jsonalready pins):WHO=1&WHO=2):11..99,0101..0915,1001..1015, with optional#4#00..#4#15local-bus suffix) on a physical BTicino wall control or touchscreen to control any non-BTicino Home Assistant entity (media_player,light,switch,fan,cover,input_boolean,lock, etc.) or execute custom Home Assistant script actions (turn_on_action,turn_off_action,step_up_action,step_down_action), while mirroring HA state back onto the bus (*1*1*W##,*1*0*W##,*#1*W*#1*...##,*2*1/2/0*W##,*#2*W*#11#001*...##).WHO=4, Dimension0):°C, within[-99.9, +99.9]°C) onto probe addresses: external probesN00(100..900,N = 1..9) or individual probesPZZ(101..899,P = 1..8,ZZ = 01..99). The group forms0(all probes),1..99(master probe of a zone) and001..099(all probes of a zone) are rejected, perownkb:claim:c003147/c001132. Frames go out via*#4*WHERE*#0*TTTT##(OWNHeatingCommand.write_temperaturewhensupports_who4_command_injection=True, or*#4*WHERE*0*TTTT##whensupports_dimension_response_injection=True). On a gateway whose profile does not set either flag toTrue(today every profile exceptF461), the binding is disabled and a warning names it at startup.WHO=18, Dimensions113,51..54&1200Streaming):dim=113, auto-convertingkWW) and energy counters (dim=51..54, auto-convertingkWhWh) at meter addresses51..5255or71#0..7255#0, and respond to Dimension1200active-power streaming handshakes (*#18*WHERE*#1200#1*DURATION##). Only on gateways whose profile setssupports_who18_response_injection=True(todayF461); elsewhere the binding is disabled with a startup warning.WHO=13, Dimensions0,1,22):0,1, and22without requiring anentity_id, answer*#13**0/1/22##queries whenrespond_to_read=True, and support a read-only query-reply mode whenperiodic_send: 0(suppressing proactive*#13**#22*...##writes on startup/reconnect for NTP-synced gateways).How is it solved
ExposeManager&ExposeBinding(custom_components/myhome/expose.py):#DIMWrite vs. Report Injection (_effective_profile,_build_frames,_async_handle_bus_message):bt_luciandbt_termo), incoming un-hashed dimension response frames (*#1*W*1*level*speed##,*#2*W*10*state*pos*001*0##,*#4*W*0*TTTT##) on the TCP command session are NACKed or parsed as dimension reads, whereas#DIMwrite frames (*#1*W*#1*level*speed##,*#2*W*#11#001*pos##,*#4*W*#0*TTTT##) are ACKed in ~83–126 ms and forwarded to the SCS bus. Conversely,WHO=18(bt_energy/bt_supervisione) does accept un-hashed*#18*W*DIM*VAL##response frames (113,51..54,1200#1).GatewayProfiledeclaressupports_dimension_response_injection,supports_who4_command_injectionandsupports_who18_response_injectionasbool | None = None, whereNonemeans unverified (no capture or oracle verdict). OnlyF461Profilesets them, from the live captures:False,True,True._injection_block_reasongatesWHO=4andWHO=18: frames are built only when the flag is exactlyTrue(forWHO=4,supports_dimension_response_injection=Truealso qualifies).False,Noneand a not-yet-known profile each block the binding.async_sync_all()(after initial discovery and on every reconnect) logs one warning per binding and reason, e.g. "Expose WHO=4 WHERE=150 dimension=0 (sensor.temp) is disabled: gateway MH202 is unverified for WHO 4 #0 temperature writes (supports_who4_command_injection is unset in OWNd). Nothing is sent for this binding." A later model correction to a verified profile re-enables the binding without a restart.supports_dimension_response_injectionnotTrue,WHO=1emitsOWNLightingCommand.set_brightness(*#1*W*#1*(100+pct)*0##, omitting a duplicate*1*0*W##whenbrightness_pct == 0) andWHO=2emitsOWNAutomationCommand.set_shutter_level(*#2*W*#11#001*pos##, answering both Dimension10and11read queries) on every gateway. Where enabled,WHO=4emitsOWNHeatingCommand.write_temperature(*#4*W*#0*TTTT##) andWHO=18emitsOWNEnergyCommand.report_*(*#18*W*113*W##,51..54,1200#1).ScriptRunner,cv.SCRIPT_SCHEMAValidation & Per-Binding Error Containment (validate.py,expose.py):turn_on_action,turn_off_action,step_up_action, andstep_down_actionat schema time viacv.SCRIPT_SCHEMA(_validate_expose_action/_coerce_action_list) and pre-compiles Home AssistantScriptrunners on startup viaasync_validate_actions_config(_async_prepare_action_scripts), unloading them cleanly viascript.async_unload()onasync_stop().data,target,service_templatewithwho,where,entity_id, andthisviaTemplateStateFromEntityId), conditions (condition: state/template),if/then/else,choose,repeat,parallel, andsequenceblocks.<domain>.<service>existence at startup (_warn_unknown_action_services, or onEVENT_HOMEASSISTANT_STARTEDwhenhass.stateis not yetCoreState.running) so mistyped service names are flagged immediately instead of only when someone presses the wall button.(HomeAssistantError, vol.Invalid)per binding in both_async_run_actionsand_async_call_service, ensuring a failing action on one binding never aborts subsequent bindings during a General (*1*0*0##) or Area (*2*2*9##) bus broadcast._EchoGuardEntry,_record_echo_guard,_is_echo_guarded):_EchoGuardEntryinstances (TX_ECHO_GUARD_WINDOW_S = 1.5s) bound toFrameSchedulerwrite futures (_attach_echo_guard_future). While a frame's write future is pending in the queue (not future.done()), incoming companion frames are guarded without popping the entry; once the socket write completes, the1.5sTTL starts fromfuturecompletion (and if cancelled before write, the guard is immediately removed).WHO=2pre-moveSTOPtranslation frame (*2*1000#0*W##before*2*1000#1/2*W##) via_pre_move_stop_guard, registers and suppresses the 6-gatewayWHO=2#11#001#11#001#1monitor translation (*#2*W*#11#001#1*LEVEL##emitted byF454,F459,F460,F461,MH202, andMyHomeServer1when*#2*W*#11#001*LEVEL##is sent oncmd), preserves sibling raw loopback guards (*WHO*WHAT*W##/*#2*W*#11#001*LEVEL##) when translation echoes arrive first, and tracks consumed gateway MACs (seen_sources) so genuine human repeat presses are never swallowed.custom_components/myhome/gateway_sessions.py):FrameSchedulersupersedes pending low-priority status frames for the same(who, where, dimension)target (_expose_replace_key) when a newer frame or high-priority confirmation is queued, cancelling the stale frame'swrittenfuture so an old mirror frame can never arrive after a wall-button press and undo it.CommandWorkerPoolmarks exposed targets (_is_exposed_target) so virtual addresses without physical bus hardware do not trigger command retries orERRORlogs on*#*0##(NACK) onMyHomeServer1/F460/F461while still resolving thewrittenfuture when the session is open.min_deltaDeadband &periodic_sendHeartbeat:last_sent_at + cooldown(_cooldown_timers) for eventual consistency, filters numeric jitter belowmin_delta, runsperiodic_sendheartbeats (900sdefault forWHO=4,86400sdefault forWHO=13in both YAML and UIExposeSubentryFlowHandler), and skips proactiveWHO=13clock writes inasync_sync_all()whenperiodic_send <= 0.validate.py,expose.py,where_grammar.py,config_flow.py):_finite_float,_non_bool_int,_non_bool_str,_validate_expose_action,_validate_expose_entry) rejects booleans disguised as integers/floats/strings, fractional ints (1.9),NaN/±infvalues (which otherwise bypass VoluptuousRange(min=..., max=...)),OverflowErrorfloats (10**400),*-containingwherestrings, invalidWHO=1/2point-to-point addresses, invalidWHO=4probe addresses (group forms0/000,50,050, probeP = 9such as901, actuator200#1, negative-105/-0, multi-####105, or 4+ digit1000/9999) or out-of-range defaults (<-99.9or>99.9), and invalidWHO=18meter addresses (50,5256,70#0,7256#0) or negative defaults.where_grammar.py(zone_number,is_probe,normalize_expose_where,expose_message_where) uses.removeprefix("4-")and.removeprefix("#")(preventing-105or###105from normalizing into valid probes), keepsis_probeat>= 100soclimateandsensorstill read001..099as zone groups, and rejectsbooland±infwithValueError. It carries no OWNd shims for expose: the frame builders, query helpers (effective_what,is_status_request,is_dimension_writing, …) and profile flags all come from OWNd ≥ 2.0.0b11, so nothing in MyHOME patchesOWNMessageor the profiles for this feature.ExposeSubentryFlowHandler(config_flow.py) andExposeBinding.from_dict(expose.py) validatewho,where,cooldown,min_delta,periodic_send,brightness_step_pct, anddefaultagainst booleans,NaN/±inf, and malformed numeric strings without raising unhandledValueError/OverflowError._build_framesand_async_handle_bus_messageguard against booleans,NaN/±inf/overflow attributes and states,WHO=1missing/None/Falsebrightness attributes,WHO=13pre-NTP years outside2000..2099,kW/kWhmultiplication overflow, and out-of-range incoming bus dimension writes (WHO=1level>200,WHO=2level>100including255unknown sentinel,WHO=18energy_typeoutside1..4ordurationoutside0..255).gateway.py,discovery.py,button.py,config_flow.py,validate.py,where_grammar.py):myhome_message_{mac}signals fordelegated_away_whos(including late-starting secondaries) and deduplicates cross-gateway RX frames within0.5s(_last_rx_by_frame).climateentities.async_sync_all()after normal initial discovery completion (outsidefinally:) and on event-session reconnects.How is it tested
tests/test_expose.py(29end-to-end & regression tests) +tests/test_f461_live_shutter_expose_trace.py(5live F461 replay tests;100.0%statement coverage onexpose.py(1056 Stmts, 0 Miss),where_grammar.py, andExposeSubentryFlowHandlerinconfig_flow.py):WHO=1&WHO=2bidirectional state mirroring (*#1*W*#1*...##and*#2*W*#11#001*...##by default, plusreport_brightness/report_shutter_levelwhensupports_dimension_response_injection=True),attribute="brightness"OFFfallback (0%/*1*0*W##),#11#001#11#001#1translation echo suppression, customScriptactions with templates ({{ this.state }},{{ entity_id }},{{ who }},{{ where }}), conditions (condition: template,if/then/else,choose,repeat,parallel,sequence), startup unknown-service warnings, per-binding runtime error containment, dimmer hold stepping (1000#30/31), preset levels (2..10), and General ("0") / Area ("1".."9","00","100") broadcasts with#4#xxisolation.test_who2_oracle_40_step_echo_loop_simulation: 40-step oracle bus simulation verifying thatWHO=2UP(*2*1000#1*98##),STOP(*2*1000#0*98##), andDOWN(*2*1000#2*98##) presses followed by multi-frame gateway echoes (*2*1000#0*98##,*2*1000#1*98##,*2*1*98##) settle in exactly 1 service call each with zero oscillation.test_run4_expose_live_f461_protocol_and_echo_guard_replay(tests/test_f461_live_shutter_expose_trace.py): replays the2026-10-18and2026-10-20live physical BTicino F461 (2.0.11) captures (tests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json, PR test(f461): add live F461 2026-10-18 trace for F411/4 lights, F411/2 5s shutter, and Expose bus verification #753, and@gdluck's2026-10-20follow-up matrix) throughExposeManager, verifying that:WHO=2event-session translation bursts (['*2*1000#0*25##', '*2*1000#1*25##']NACK) are absorbed with zero self-oscillation.*#2*25*#11#001*50##shutter position writes and their F461 event-session translation*#2*25*#11#001#1*50##are absorbed by_tx_echo_guardwithout looping back intocover.set_cover_position.test_expose_infeasible_inputs_hardening: verifies thatexpose_entry_schema,ExposeSubentryFlowHandler,ExposeBinding.from_dict,_build_frames,_async_handle_bus_message,where_grammar.py, andOWN*Commandbuilders reject booleans, fractional ints,NaN/±inf/10**400, negative/multi-#probe addresses, out-of-range numeric values, and malformedwhereaddresses.test_unverified_gateway_disables_who4_who18_with_one_warning: on anMH202profile (flags unset)WHO=4/WHO=18bindings send nothing whileWHO=1still syncs; exactly one warning per binding across repeated syncs; distinct reasons for aFalseflag and for a missing profile; and once the profile resolves toF461the same bindings emit*#4*150*#0*0215##/*#18*51*113*450##with no further warning. The test gateway helper defaults toF461Profile, the only profile with verified flags, and the shared-bus delegation test now delegatesWHO=18to anF461secondary.ONpress supersedes a still-queued status-tierOFFmirror frame so a staleOFFnever executes afterON, and that late startup/reconnect echoes are guarded until1.5safter the write future resolves.min_deltadeadband filtering,periodic_sendtimer firing,WHO=4positive/negative temperature encoding (*#4*W*#0*TTTT##write default +report_temperaturewhensupports_dimension_response_injection=True),WHO=18active power/energy +DIMENSION 1200streaming lifecycle,WHO=13clock sync (periodic_send > 0vsperiodic_send == 0read-only skip) and*#13**0/1/22##query replies, shared-bus secondary gateway delegation & RX deduplication, phantom entity/button suppression, YAML validation, andExposeSubentryFlowHandlerUI flows.2.0.0b11wheel (themanifest.jsonpin), merged with the currentv2-phase1-architecture: 4388 passed, 100.0% statement coverage ofcustom_components/myhome.python scripts/typing_ratchet.pymypy --strict: 0 error(s) in 0 module(s); baseline 0, checked against the pinned OWNd2.0.0b11types as CI has run it since fix(typing): check mypy --strict against the pinned OWNd 2.0.0b11 types #765 (the frame lists andcmd/confirmare typedOWNCommand)ruff check custom_components tests scriptsAll checks passed!python scripts/verify_ha_standards.pySUCCESS: All Home Assistant architectural standards passed!python scripts/sync_documentation.py --checkSUCCESS: All documentation is calibrated and in sync with the codebase!State Diagram
1. Write-Future-Bound Echo Guard & Virtual Actuator State Machine (
WHO=1/WHO=2)stateDiagram-v2 [*] --> Synced : async_sync_all() state "Synced (HA state == Bus state)" as Synced state "Queued in FrameScheduler (future pending)" as QueuedTX state "Written to Gateway Socket (1.5s TTL active)" as WrittenTX state "Bus Command Intercepted (*WHO*1000#WHAT*W##)" as BusCommand Synced --> QueuedTX : HA state_changed -> _async_dispatch_frames()\n1. Supersede any stale queued TX for (who, where, dim)\n2. Arm _EchoGuardEntry (raw + 1000#what + WHO=2 pre-move STOP + #11#001#1)\n3. Attach future callback QueuedTX --> Synced : Superseded or Cancelled before socket write\n-> Immediately remove _EchoGuardEntry QueuedTX --> WrittenTX : Socket write completes (future.done())\n-> Start 1.5s TTL from now WrittenTX --> WrittenTX : Monitor/Command echo arrives:\n• *2*1000#0*W## -> consume _pre_move_stop_guard\n• *WHO*1000#WHAT*W## or *#2*W*#11#001#1*L## -> consume trans_entry (keep sibling raw_entry!)\n• *WHO*WHAT*W## or *#2*W*#11#001*L## -> consume raw_entry WrittenTX --> Synced : All echo forms consumed or 1.5s TTL expires Synced --> BusCommand : Physical Button Press (*WHO*1000#WHAT*W## or *WHO*WHAT*W##) BusCommand --> QueuedTX : 1. Run HA Script / service (templates, conditions, error-contained)\n2. Queue virtual actuator confirmation (Priority.HIGH, no NACK retry)2. Trailing-Edge Cooldown,
min_deltaDeadband &periodic_sendState MachinestateDiagram-v2 [*] --> Idle : Binding Initialized state "Idle (Cooldown Elapsed)" as Idle state "Deadband Check (|val - last_sent| >= min_delta?)" as DeadbandCheck state "Cooldown Active (No Pending Value)" as CooldownActive state "Cooldown Active (Trailing-Edge Timer Armed)" as TrailingPending Idle --> DeadbandCheck : HA state_changed(new_val) DeadbandCheck --> Idle : |new_val - last_sent| < min_delta\n(Filtered out) DeadbandCheck --> CooldownActive : Delta >= min_delta\nDispatch Frame (last_sent_at = now) CooldownActive --> TrailingPending : state_changed(val) within cooldown\nSchedule _fire_trailing at (last_sent_at + cooldown) TrailingPending --> TrailingPending : Additional state_changed(latest_val)\nCoalesced until trailing-edge timer fires TrailingPending --> CooldownActive : Cooldown timer fires\nDispatch latest HA state (Eventual Consistency!) CooldownActive --> Idle : Cooldown window elapsed Idle --> CooldownActive : periodic_send timer fires\nRe-publish current stateWhere are the traces
MyHOMEown-firmware-oracle)2.0.11)Expose(WHO=1, 2, 4, 13, 18),F411/4Lights &F411/25 s Shutter (2026-10-18&2026-10-20)tests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json(PR #753 &issuecomment-6277050571)run1_lights_group_and_general,run2_shutter_05_travel_and_reversal,run3_climate_zone2,run4_expose_virtual_bus_verificationWHO=1Command Translation (1000#30/31) & Dimmer Stepstests/fixtures/traces/issue_466/myhome_trace_MH202_f418u2_dimmer_2026-09-27T09-43-45.jsontests/fixtures/traces/issue_466/myhome_trace_H4890_all_2026-09-26T11-51-26.jsonresults/*/oracle/full/lights-level.tsv(*1*1*31##,MH202*1*1000#1*31##session response)WHO=2Shutter Command Translation, Pre-MoveSTOP#001#1Level Echotests/fixtures/traces/issue_645/myhome_trace_MyHomeServer1_cover_direction_then_stop_2026-10-11.jsontests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.jsonresults/*/oracle/full/trace-corpus.tsv(*2*1*31##*2*1000#0*31##, *2*1000#1*31##on 10 gateways;*#2*31*#11#001*40##*#2*31*#11#001#1*40##onF454/F459/F460/F461/MH202/MyHomeServer1)WHO=4Temperature ProbeDIMENSION 0tests/fixtures/traces/issue_429/myhome_trace_MyHomeServer1_who4_2026-09-23T18-13-49.jsontests/fixtures/traces/issue_619/myhome_trace_F453AV_who4_2026-10-11T10-29-57.jsonresults/*/oracle/full/thermo-ts10.tsv(*#4*1*0##$0499013000\r)WHO=18Active Power (113) & Streaming (1200)tests/fixtures/traces/issue_466/myhome_trace_F454_who18_2026-10-08T19-30-16.jsonresults/*/oracle/full/energy-ts10.tsv(*#18*51*113##$06D1A10233001D0000\r)NACKtests/fixtures/traces/issue_466/MyHomeServer1_dead_zone_nacks_2026-09-29T22-45.txttests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.jsonresults/MyHomeServer1/028206/oracle/full/lights-level.tsv(Reply: NACK (*#*0##),Verdict: OUT (Bus))What does the Oracle / Encyclopedia say
1. OpenWebNet Encyclopedia Machine KB Claims (
OpenWebNet-Encyclopedia/knowledge/claims)ownkb:claim:c000114–c000118corroborated_interpretation*WHO*WHAT*WHERE##(command/state),*#WHO*WHERE##(status request),*#WHO*WHERE*DIMENSION##(dimension request),*#WHO*WHERE*DIMENSION*VALUE...##(dimension response), and*#WHO*WHERE*#DIMENSION*VALUE...##(dimension write).ownkb:claim:c001440,c001442specifiedWHO=1Command Translation (1000#INNER_WHAT) wraps translated lighting/dimmer commands (*1*1000#WHAT*WHERE##).ownkb:claim:c002643,c002647specifiedWHO=2Command Translation (1000#INNER_WHAT) wraps translated automation commands (*2*1000#0/1/2*WHERE##).ownkb:claim:c001132,c001133,c003172,c003235,ownkb:claim:moc-c007713specified/observedWHO=4External probe class uses[PL_N]00;DIMENSION 0encodes measured temperature in 4-digit tenths of°C(0000..0500inc003235; valuesmoc-c007713).ownkb:claim:c002222,c002264,c002287,ownkb:claim:moc-c007644specified/observedWHO=18DIMENSION 113reports active power in Watts (*#18*W*113*WATT##) and is emitted duringDIMENSION 1200active-power streaming;DIMENSION 51..54report energy totalizers in Wh.ownkb:claim:c001598–c001604,c001674–c001676,c001742–c001743specifiedWHO=13DIMENSION 22(H*M*S*T*W*D*M*Y) combinesDIMENSION 0(time + timezone) andDIMENSION 1(weekday + date) in a single atomic R/W frame.2. Empirical Firmware Oracle & Live BTicino F461 (
2.0.11,2026-10-18&2026-10-20) Hardware Verdicts*#WHO*WHERE*DIM*VAL##) vs.#DIMWrite Form (*#WHO*WHERE*#DIM*VAL##) on Physical F461 (2.0.11):WHO=1:*#1*08*1*150*0##(report form) is rejected withNACKin 5 ms, whereas*#1*08*#1*150*0##(set_brightness#1write form) is ACKed in 88–126 ms.WHO=2:*#2*25*10*10*50*001*0##(report form) is rejected withNACKin 82 ms, whereas*#2*25*#11#001*50##(set_shutter_level#11#001write form) is ACKed in 83 ms and echoed onevtas*#2*25*#11#001#1*50##.WHO=4:*#4*2*0*0999##and*#4*50*0*0215##(report form without#0) are parsed bybt_termoas Dimension0read requests (*#4*2*0*0999##returns Zone 2's measured temperature*#4*2*0*0249##), whereas*#4*50*#0*0215##(write_temperature#0write form) is ACKed in 84–155 ms.WHO=18: Unlikebt_luciandbt_termo,bt_energy/bt_supervisionedoes accept un-hashed dimension response frames (*#18*51*113*450##,51..54,1200#1), which are ACKed in 140 ms – 1.7 s on F461.WHO=2Virtual Cover (WHERE="25", unpopulated vs physicalBT-F411/2atWHERE="05"):F411/2(WHERE="05", 5 s travel cutoff):*2*1*05##emits['*2*1000#0*05##', '*2*0*05##', '*2*1000#1*05##', '*2*1*05##']onevt, and a mid-flight reversal (*2*1*05##*2*2*05##) is followed 5.0 s later by the hardware relay's spontaneous end-of-travel*2*0*05##.WHERE="25", no physical actuator): sending*2*1*25##,*2*2*25##, or*2*0*25##on the live F461 immediately broadcasts['*2*1000#0*25##', '*2*1000#1*25##'](sometimes twice before the ~0.55 sNACK),['*2*1000#0*25##', '*2*1000#2*25##'], and['*2*1000#0*25##']on theevtmonitor session. Replaying these exactevtbursts throughExposeManagerconfirms that_pre_move_stop_guardand_tx_echo_guardabsorb all translation frames with zero self-oscillation.expose.py):WHEREand answeringrespond_to_readstatus queries from a TCP monitor session remain best-effort subject to gateway/control firmware.turn_on_action/turn_off_action/step_up_action/step_down_actionentries are validated viacv.SCRIPT_SCHEMA/async_validate_actions_configand executed via Home Assistant'sScripthelper (with sandboxedTemplateevaluation) in response to bus frames, and should only be configured on trusted physical/network SCS installations.