Repository navigation
feat(who14): implement WHO 14 actuator lock and unlock - #130
Open
GreenGrassBlueOcean wants to merge 1 commit into
Open
GreenGrassBlueOcean wants to merge 1 commit into
GreenGrassBlueOcean wants to merge 1 commit into
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
GreenGrassBlueOcean
force-pushed
the
feat/who14-actuator-lock
branch
from
October 18, 2026 20:01
4a01bc2 to
241ed57
Compare
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
Downstream actuator-lock control, scenario automation, and bus telemetry decoding require first-class protocol support for WHO=14 (
Special commands/ Actuator Lock & Unlock) that was previously missing inOWNd:WHO=14(Special commands/ Actuator Lock & Unlock) subsystem implementation (OWNd/message/actuator_lock.py):OWNdpreviously fell back to genericOWNMessage/OWNCommandinstances forWHO=14(if _who in (0, 14, 24) or _who > 1000: return cls(data)inOWNd/message/base.py), providing no typed event parser (OWNActuatorLockEvent/OWNSpecialEvent), no command builders (OWNActuatorLockCommand.lock(),.unlock(),.set_locked(),.disable(),.enable()), no state constants (ACTUATOR_STATE_LOCKED = 0,ACTUATOR_STATE_UNLOCKED = 1), and no human-readable descriptions.WHO=14wire semantics, SCS bus translation, and addressing boundaries:WHO=14, leaving itsWHATsemantics (0= lock/disable actuator,1= unlock/enable actuator), SCS bus opcodes ($03<addr>001100/$003<addr>001100/$04<addr>001100for lock;$03<addr>001000/$003<addr>001000/$04<addr>001000for unlock), and addressing constraints (Point-to-PointAPLand#4#01..09local bus interface addresses only; status queries*#14*WHERE##andWHAT >= 2rejected bybt_luci's flex scanner) dispersed acrossOPEN.db,ScenarioDevices,bt_luci/cosofirmware binaries, andown-firmware-oracle.own-firmware-oracleNACK + OUTvs physical hardwareACK + OUTonF461/F460/MyHomeServer1:own-firmware-oracle, sending*14*0*32##or*14*1*32##without a physical actuator wired to the emulated SCS bus producesverdict: "out", reply: "nack"onF460,F461, andMyHomeServer1(after a ~2.05 s bus ACK timeout), whereasF453,F453AV,F454,F459,MH200,MH200N, andMH202returnverdict: "out", reply: "ack"immediately. Live verification on a physical BTicinoF461(2.0.11) plant with wiredBT-F411/4andBT-F411/2actuators (OpenWebNet-HA/MyHOME#752) confirmed that when a populated actuator is present atWHERE,F461returns*#*1##(ACK) in89–111 msand broadcasts*14*0/1*WHERE##on the event monitor session27–42 mslater.WHO=14builders (extendingOpenWebNet-HA/OWNd#128):WHO=14command builders must enforce the same strict input boundaries established inOWNd#128: rejectingboolvalues (True/Falsedisguised asintforwhereorinterface, or non-boolvalues forlocked), empty or*-pollutedWHEREstrings, non-ASCII Unicode digits (e.g. Arabic-Indic"١٢"), General ("0"), Area ("1"–"9","00","100"), Group ("#1"), out-of-rangeAPLaddresses ("10","0010","916"), bare#4#suffixes, and local bus interfaces outside1..9(0,10,"00","1","10").How is it solved
WHO=14Subsystem Module (OWNd/message/actuator_lock.py):OWNActuatorLockEvent(aliasOWNSpecialEvent,@register_event(14)):*14*0*WHERE##(WHAT=0, lock/disable) and*14*1*WHERE##(WHAT=1, unlock/enable), plus pushbutton command-translation frames*14*1000#0*WHERE##and*14*1000#1*WHERE##viaeffective_what.effective_what(int | None),is_lock(bool),is_unlock(bool),is_locked(bool | None:TrueforWHAT=0,FalseforWHAT=1,Nonefor unknownWHATor dimension frames),lock_state("locked"/"unlocked"/None),bus_interface(str | None, extracting"01".."09"fromWHERE#4#IF), andactuator_address(str | None, extracting the baseAPLaddress before#4#).as_human_readableformatting (e.g."Actuator 07: Locked (disabled)","Actuator 07 (bus interface 01): Unlocked (enabled)", and[command translation]suffix whenis_translationis true).OWNActuatorLockCommand(aliasOWNSpecialCommand,@register_command(14)):lock(where, *, interface=None)*14*0*WHERE##or*14*0*WHERE#4#01..09##unlock(where, *, interface=None)*14*1*WHERE##or*14*1*WHERE#4#01..09##set_locked(where, locked, *, interface=None)lock()(locked=True) orunlock()(locked=False), rejecting non-boollockedinputs (1,0,"true") withValueError.disable(where, *, interface=None)andenable(where, *, interface=None)convenience aliases forlock()andunlock().effective_what,is_lock,is_unlock,is_locked,lock_state,bus_interface,actuator_address, andas_human_readableproperties asOWNActuatorLockEvent._format_actuator_whereinOWNd/message/actuator_lock.py):boolinputs forwhereandinterfacebefore string conversion.require_non_empty_where(where)and.isascii()on both the baseAPLaddress and any#4#IFinterface token.is_apl_address(base)(11–99,01–09,0110–0915,1010–1015), rejecting General ("0"), Area ("1"–"9","00","100"), Group ("#1"), and malformedAPLaddresses withValueError.#4#IFlocal bus interfaces as integer1..9(formatted as zero-padded01..09) or 2-digit ASCII"01".."09", and rejects passing both an embedded#4#IFinwhereand theinterface=keyword argument.OWNd/message/base.py,OWNd/message/__init__.py):"actuator_lock"to_WHO_SUBMODULESand removed14from the generic fallback tupleif _who in (0, 24) or _who > 1000:inOWNCommand.parseso allWHO=14frames dispatch toOWNActuatorLockEvent/OWNActuatorLockCommand.ACTUATOR_LOCK_STATE_NAMES,ACTUATOR_STATE_LOCKED,ACTUATOR_STATE_UNLOCKED,OWNActuatorLockCommand,OWNActuatorLockEvent,OWNSpecialCommand, andOWNSpecialEventinOWNd.message.__all__.scripts/sync_documentation.py,README.md):WHO 14(Special Commands (Actuator Lock / Unlock)) inWHO_DEFINITIONSand synchronizedREADME.md.State diagram
stateDiagram-v2 direction TB state "1. Outbound WHO=14 Command Builder & Gateway SCS Translation" as CommandFlow { [*] --> BuilderCall: lock(where, interface=...) / unlock(where, interface=...) / set_locked(...) BuilderCall --> ValidateWhere: _format_actuator_where(where, interface) ValidateWhere --> RejectValueError: bool / non-ASCII / General / Area / Group / invalid APL or #4#IF ValidateWhere --> EmitFrame: Valid Point-to-Point APL or APL#4#01..09 (*14*0/1*WHERE##) EmitFrame --> FlexScanner: Gateway bt_luci / coso flex DFA (rules 201-203, 232-234) FlexScanner --> ImmediateNack: Status query (*#14*W##) or WHAT >= 2 -> *#*0## (~50 ms) FlexScanner --> ScsBusTransmit: Emit $03<addr>001100 (lock) or $03<addr>001000 (unlock) ScsBusTransmit --> PopulatedActuatorAck: Actuator at WHERE ACKs on SCS bus -> *#*1## (89-111 ms) + monitor *14*0/1*WHERE## ScsBusTransmit --> UnpopulatedTimeoutNack: No actuator at WHERE on F460/F461/MHS1 -> *#*0## (~2.05 s timeout, 0 monitor frames) RejectValueError --> [*] ImmediateNack --> [*] PopulatedActuatorAck --> [*] UnpopulatedTimeoutNack --> [*] } state "2. Physical Actuator Lock Lifecycle (F411/4 Lighting & F411/2 Shutter)" as ActuatorLifecycle { [*] --> Unlocked: Default / *14*1*WHERE## (WHAT=1, is_locked=False) Unlocked --> Locked: *14*0*WHERE## (WHAT=0, is_locked=True) state Locked { [*] --> F411_4_Light: BT-F411/4 (WHERE 06, 07) [*] --> F411_2_Shutter: BT-F411/2 (WHERE 05, 5 s shutter) F411_4_Light --> IgnoreSwitch: Ignores *1*1*07## / *1*0*06## (state frozen) F411_2_Shutter --> AutoStopAndFreeze: Emits *2*0*05## STOP after 54 ms & blocks *2*1/2*05## } Locked --> Unlocked: *14*1*WHERE## (WHAT=1, is_locked=False -> normal WHO=1 & WHO=2 actuation restored) }How is it tested
tests/test_who14_actuator_lock.py):test_event_parse_lock_and_unlock,test_event_parse_with_bus_interface_4,test_event_parse_4digit_apl_addresses: verifiesOWNActuatorLockEventparsing across 2-digit (06,07,05,32), 4-digit (0115,1015), and#4#01..09local bus interface addresses.test_event_parse_translation_frames: verifies1000#0and1000#1command-translation frames unwrapeffective_whatto0/1and append[command translation]inas_human_readable.test_event_unknown_what_and_dimension_frames: verifiesWHAT >= 2, non-numericWHAT, and*#14*WHERE##status/dimension frames returnis_locked = None,lock_state = None, and safe fallback human-readable descriptions.test_command_builders_lock_unlock_aliases_and_set_locked: verifieslock(),unlock(),disable(),enable(), andset_locked()acrossstr/intaddresses andinterfaceparameters, plusOWNCommand.parseroundtrip parity.test_command_builder_rejects_invalid_inputs: exhaustively tests rejection (ValueError) ofboolinputs (where=True,interface=True,locked=1), non-ASCII Unicode digits ("١٢",interface="١"), empty/wildcardWHERE, General ("0"), Area ("1","00","100"), Group ("#1"), invalidAPL("10","0010","916"), bare"07#4#", duplicate"#4#", and out-of-range interfaces (0,10,"00","1","10","ab").tests/test_who14_actuator_lock.py,tests/test_firmware_oracle_conformance.py):test_firmware_oracle_who14_entries_and_roundtrip: verifies all 24WHO=14rows intests/golden/firmware_oracle.json(*14*0*32##and*14*1*32##across all 12 emulated gateway firmwares) haveverdict == "out"and 100% parser/builder roundtrip parity.tests/test_isolated_imports.py,tests/test_sync_documentation.py):OWNd.message.actuator_lockimports cleanly in isolation andREADME.mdstays synchronized withscripts/sync_documentation.py.1,786 passed, 1 skippedacross Python 3.11–3.14 with 100.0% statement and branch coverage onOWNd/message/actuator_lock.py(117 Stmts, 0 Miss, 38 Branch, 0 BrPart) andOWNd/message/base.py,mypy OWNdclean (0 issues),ruff checkclean, andMyHOMEconformance workflow (Golden corpus & smoke gates+MyHOME test suite) passing.Where are the traces
1. Real Physical Bus Captures by Community Contributors (
community-plant-capture)Following the repository convention of keeping raw hardware trace archives in
OpenWebNet-HA/MyHOME, the anonymized liveF461(2.0.11) +BT-F411/4+BT-F411/2bus captures forWHO=14were ingested in a separateMyHOMEPR (OpenWebNet-HA/MyHOME#752):MyHOME/tests/fixtures/traces/)WHO=14Lighting Actuator Lock (BT-F411/4)f461_who14_live_2026-10-18/myhome_trace_F461_who14_live_2026-10-18.json(OpenWebNet-HA/MyHOME#752,run2_who14_lighting_f411_4)@gdluck(Aurel) &@GreenGrassBlueOcean(MyHOME#752) — BTicinoF461(FW2.0.11,WHO=13code200,WHO=1013134.15.5.0) +BT-F411/44-channel lighting actuator (WHERE 06, 07)*14*0*07##(cmdACKin93 ms+evtecho*14*0*07##29 mslater; blocks*1*1*07##while locked OFF),*14*1*07##(cmdACKin91 ms+evtecho*14*1*07##; restores*1*1*07##),*14*0*06##(cmdACKin89 ms+evtecho*14*0*06##; blocks*1*0*06##while locked ON),*14*1*06##(cmdACKin96 ms+evtecho*14*1*06##)WHO=14Shutter Actuator Lock (BT-F411/25 s Shutter)f461_who14_live_2026-10-18/myhome_trace_F461_who14_live_2026-10-18.json(OpenWebNet-HA/MyHOME#752,run3_who14_shutter_f411_2)@gdluck(Aurel) &@GreenGrassBlueOcean(MyHOME#752) — BTicinoF461(FW2.0.11) +BT-F411/2shutter actuator (WHERE 05, 5 s travel time)*14*0*05##(cmdACKin105 ms+evtecho*14*0*05##+ automatic hardware*2*0*05##STOP54 mslater; blocks*2*1*05##UP and*2*2*05##DOWN while locked),*14*1*05##(cmdACKin111 ms+evtecho*14*1*05##; restores full*2*1000#0*05##*2*0*05##*2*1000#1/2*05##*2*1/2*05##*2*0*05##cycle)WHO=14Interface Routing (#4#01), Unpopulated Timeout & Parser Boundariesf461_who14_live_2026-10-18/myhome_trace_F461_who14_live_2026-10-18.json(OpenWebNet-HA/MyHOME#752,run4_who14_boundaries_and_routing)@gdluck(Aurel) &@GreenGrassBlueOcean(MyHOME#752) — BTicinoF461(FW2.0.11)*14*0*07#4#01##(cmdACKin108 ms),*14*1*07#4#01##(cmdACKin101 ms), unpopulated*14*0*99##/*14*1*99##(cmdNACKafter2,053 ms/2,052 msbus timeout,0monitor frames), status query*#14*07##(cmdNACKin53 ms), and invalidWHAT=2*14*2*07##(cmdNACKin50 ms)2. Emulated Gateway Firmware Oracle Traces (
own-firmware-oracle)Command-session forwarding behavior across all 12 emulated gateway firmwares is pinned in
tests/golden/firmware_oracle.json(sourced fromown-firmware-oracle/results/<PRODUCT>/<VERSION>/oracle/full/special-who14.tsv):WHO=14Special Commands (special-who14.tsv):*14*0*32##($0332001100\r/$00332001100\r) and*14*1*32##($0332001000\r/$00332001000\r) across all 12 gateways (F450,F453,F453AV,F454,F459,F460,F461,H4684,MH200,MH200N,MH202,MyHomeServer1).What does the Encyclopedia / Firmware Oracle / Live F461 Plant say
OpenWebNet-Encyclopedia+OPEN.db+ScenarioDevices+bt_luci/cosoBinary Analysis):OPEN.db&ScenarioDevices(ownkb:claim:c000883..c000886,ownkb:claim:c000899..c000902):OPEN.dbdefinesIDWHO = 14as"Special commands". MyHOME_SuiteScenarioDevices(3.5.38) bindsminiScenarioSuite.specialCommands.actionLockUnlockActuator.lockto*14*0*WHERE##(WHAT = 0, lock/disable actuator) and.unlockto*14*1*WHERE##(WHAT = 1, unlock/enable actuator).ownkb:claim:c002015..c002022): Inbt_luci(F453010100,F454020005,MH200N010108,F461020011) andcoso(F450020018,H4684060004), flex scanner rules201–203("Messaggio openwebnet comando spec") accept onlyWHAT = 0orWHAT = 1with Point-to-PointWHERE(11–99,01–09,0110–0915,1010–1015) and optional#4#01..09local bus interface routing (*14*[01]*WHERE(#4#0[1-9])?##), and rules232–234,341,362("Messaggio scs illuminazione comando speciale") translate symmetrically between OpenWebNet and SCS opcodes001100(WHAT=0lock) and001000(WHAT=1unlock). Status requests (*#14*WHERE##) have no flex rule and are immediately NACKed.own-firmware-oracle) & Live F461 Hardware Verification (MyHOME#752):*14*0*WHERE##(Lock) Reply / Verdict*14*1*WHERE##(Unlock) Reply / VerdictLock/Unlock)F450(020018),H4684(060004)cosoNACK/OUTNACK/OUT$0332001100\r/$0332001000\rcosoparser forwards onto SCS busF453(010100),F453AV(010035),MH200(020100),MH200N(010108)bt_luciACK/OUTACK/OUT$0332001100\r/$0332001000\rACK+ standard 7-byte SCS telegramF454(020005),F459(010019),MH202(020613)bt_luciACK/OUTACK/OUT$00332001100\r/$00332001000\rACK+ extended$003SCS telegramF460(020007),F461(020011),MyHomeServer1(023809) (unpopulatedWHERE 32/99)bt_luciNACK/OUTNACK/OUT$00332001100\r/$00332001000\r*#*0##when unpopulatedF461(020011) + PhysicalBT-F411/4(06, 07) &BT-F411/2(05) (MyHOME#752)bt_luciACK(89–108 ms) +evtechoACK(91–111 ms) +evtecho$003<addr>001100\r/$003<addr>001000\r($04<addr>01001100\ron#4#01)F461returns*#*1##oncmdand echoes*14*0/1*WHERE##onevt