Skip to content

feat(who14): implement WHO 14 actuator lock and unlock - #130

Open
GreenGrassBlueOcean wants to merge 1 commit into
masterfrom
feat/who14-actuator-lock
Open

GreenGrassBlueOcean wants to merge 1 commit into
masterfrom
feat/who14-actuator-lock

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

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 in OWNd:

  1. Missing WHO=14 (Special commands / Actuator Lock & Unlock) subsystem implementation (OWNd/message/actuator_lock.py):
    • OWNd previously fell back to generic OWNMessage / OWNCommand instances for WHO=14 (if _who in (0, 14, 24) or _who > 1000: return cls(data) in OWNd/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.
  2. Undocumented WHO=14 wire semantics, SCS bus translation, and addressing boundaries:
    • Public Legrand/BTicino PDF sheets omit WHO=14, leaving its WHAT semantics (0 = lock/disable actuator, 1 = unlock/enable actuator), SCS bus opcodes ($03<addr>001100 / $003<addr>001100 / $04<addr>001100 for lock; $03<addr>001000 / $003<addr>001000 / $04<addr>001000 for unlock), and addressing constraints (Point-to-Point APL and #4#01..09 local bus interface addresses only; status queries *#14*WHERE## and WHAT >= 2 rejected by bt_luci's flex scanner) dispersed across OPEN.db, ScenarioDevices, bt_luci / coso firmware binaries, and own-firmware-oracle.
  3. Reconciling own-firmware-oracle NACK + OUT vs physical hardware ACK + OUT on F461 / F460 / MyHomeServer1:
    • In own-firmware-oracle, sending *14*0*32## or *14*1*32## without a physical actuator wired to the emulated SCS bus produces verdict: "out", reply: "nack" on F460, F461, and MyHomeServer1 (after a ~2.05 s bus ACK timeout), whereas F453, F453AV, F454, F459, MH200, MH200N, and MH202 return verdict: "out", reply: "ack" immediately. Live verification on a physical BTicino F461 (2.0.11) plant with wired BT-F411/4 and BT-F411/2 actuators (OpenWebNet-HA/MyHOME#752) confirmed that when a populated actuator is present at WHERE, F461 returns *#*1## (ACK) in 89–111 ms and broadcasts *14*0/1*WHERE## on the event monitor session 27–42 ms later.
  4. Infeasible parameter and address bounds on WHO=14 builders (extending OpenWebNet-HA/OWNd#128):
    • WHO=14 command builders must enforce the same strict input boundaries established in OWNd#128: rejecting bool values (True/False disguised as int for where or interface, or non-bool values for locked), empty or *-polluted WHERE strings, non-ASCII Unicode digits (e.g. Arabic-Indic "١٢"), General ("0"), Area ("1"–"9", "00", "100"), Group ("#1"), out-of-range APL addresses ("10", "0010", "916"), bare #4# suffixes, and local bus interfaces outside 1..9 (0, 10, "00", "1", "10").

How is it solved

  1. WHO=14 Subsystem Module (OWNd/message/actuator_lock.py):
    • OWNActuatorLockEvent (alias OWNSpecialEvent, @register_event(14)):
      • Parses *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## via effective_what.
      • Exposes typed properties: effective_what (int | None), is_lock (bool), is_unlock (bool), is_locked (bool | None: True for WHAT=0, False for WHAT=1, None for unknown WHAT or dimension frames), lock_state ("locked" / "unlocked" / None), bus_interface (str | None, extracting "01".."09" from WHERE#4#IF), and actuator_address (str | None, extracting the base APL address before #4#).
      • Human-readable as_human_readable formatting (e.g. "Actuator 07: Locked (disabled)", "Actuator 07 (bus interface 01): Unlocked (enabled)", and [command translation] suffix when is_translation is true).
    • OWNActuatorLockCommand (alias OWNSpecialCommand, @register_command(14)):
      • Declarative command builders:
        • lock(where, *, interface=None) $\to$ *14*0*WHERE## or *14*0*WHERE#4#01..09##
        • unlock(where, *, interface=None) $\to$ *14*1*WHERE## or *14*1*WHERE#4#01..09##
        • set_locked(where, locked, *, interface=None) $\to$ delegates to lock() (locked=True) or unlock() (locked=False), rejecting non-bool locked inputs (1, 0, "true") with ValueError.
        • disable(where, *, interface=None) and enable(where, *, interface=None) convenience aliases for lock() and unlock().
      • Exposes the same effective_what, is_lock, is_unlock, is_locked, lock_state, bus_interface, actuator_address, and as_human_readable properties as OWNActuatorLockEvent.
  2. Infeasible Input & Address Boundary Validation (_format_actuator_where in OWNd/message/actuator_lock.py):
    • Rejects bool inputs for where and interface before string conversion.
    • Enforces require_non_empty_where(where) and .isascii() on both the base APL address and any #4#IF interface token.
    • Requires is_apl_address(base) (11–99, 01–09, 0110–0915, 1010–1015), rejecting General ("0"), Area ("1"–"9", "00", "100"), Group ("#1"), and malformed APL addresses with ValueError.
    • Validates optional #4#IF local bus interfaces as integer 1..9 (formatted as zero-padded 01..09) or 2-digit ASCII "01".."09", and rejects passing both an embedded #4#IF in where and the interface= keyword argument.
  3. Base Message Dispatch & Package Exports (OWNd/message/base.py, OWNd/message/__init__.py):
    • Added "actuator_lock" to _WHO_SUBMODULES and removed 14 from the generic fallback tuple if _who in (0, 24) or _who > 1000: in OWNCommand.parse so all WHO=14 frames dispatch to OWNActuatorLockEvent / OWNActuatorLockCommand.
    • Exported ACTUATOR_LOCK_STATE_NAMES, ACTUATOR_STATE_LOCKED, ACTUATOR_STATE_UNLOCKED, OWNActuatorLockCommand, OWNActuatorLockEvent, OWNSpecialCommand, and OWNSpecialEvent in OWNd.message.__all__.
  4. Documentation Catalog Synchronization (scripts/sync_documentation.py, README.md):
    • Registered WHO 14 (Special Commands (Actuator Lock / Unlock)) in WHO_DEFINITIONS and synchronized README.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)
    }
Loading

How is it tested

  • Unit & Boundary Tests (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: verifies OWNActuatorLockEvent parsing across 2-digit (06, 07, 05, 32), 4-digit (0115, 1015), and #4#01..09 local bus interface addresses.
    • test_event_parse_translation_frames: verifies 1000#0 and 1000#1 command-translation frames unwrap effective_what to 0 / 1 and append [command translation] in as_human_readable.
    • test_event_unknown_what_and_dimension_frames: verifies WHAT >= 2, non-numeric WHAT, and *#14*WHERE## status/dimension frames return is_locked = None, lock_state = None, and safe fallback human-readable descriptions.
    • test_command_builders_lock_unlock_aliases_and_set_locked: verifies lock(), unlock(), disable(), enable(), and set_locked() across str/int addresses and interface parameters, plus OWNCommand.parse roundtrip parity.
    • test_command_builder_rejects_invalid_inputs: exhaustively tests rejection (ValueError) of bool inputs (where=True, interface=True, locked=1), non-ASCII Unicode digits ("١٢", interface="١"), empty/wildcard WHERE, General ("0"), Area ("1", "00", "100"), Group ("#1"), invalid APL ("10", "0010", "916"), bare "07#4#", duplicate "#4#", and out-of-range interfaces (0, 10, "00", "1", "10", "ab").
  • Firmware Oracle Conformance Tests (tests/test_who14_actuator_lock.py, tests/test_firmware_oracle_conformance.py):
    • test_firmware_oracle_who14_entries_and_roundtrip: verifies all 24 WHO=14 rows in tests/golden/firmware_oracle.json (*14*0*32## and *14*1*32## across all 12 emulated gateway firmwares) have verdict == "out" and 100% parser/builder roundtrip parity.
  • Isolated Import & Documentation Sync Tests (tests/test_isolated_imports.py, tests/test_sync_documentation.py):
    • Verifies OWNd.message.actuator_lock imports cleanly in isolation and README.md stays synchronized with scripts/sync_documentation.py.
  • Full Suite & Static Analysis:
    • 1,786 passed, 1 skipped across Python 3.11–3.14 with 100.0% statement and branch coverage on OWNd/message/actuator_lock.py (117 Stmts, 0 Miss, 38 Branch, 0 BrPart) and OWNd/message/base.py, mypy OWNd clean (0 issues), ruff check clean, and MyHOME conformance 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 live F461 (2.0.11) + BT-F411/4 + BT-F411/2 bus captures for WHO=14 were ingested in a separate MyHOME PR (OpenWebNet-HA/MyHOME#752):

Subsystem Real Community Capture Files (MyHOME/tests/fixtures/traces/) Contributor & Hardware Plant Captured Wire Frames Verified by This PR
WHO=14 Lighting 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) — BTicino F461 (FW 2.0.11, WHO=13 code 200, WHO=1013 134.15.5.0) + BT-F411/4 4-channel lighting actuator (WHERE 06, 07) *14*0*07## (cmd ACK in 93 ms + evt echo *14*0*07## 29 ms later; blocks *1*1*07## while locked OFF), *14*1*07## (cmd ACK in 91 ms + evt echo *14*1*07##; restores *1*1*07##), *14*0*06## (cmd ACK in 89 ms + evt echo *14*0*06##; blocks *1*0*06## while locked ON), *14*1*06## (cmd ACK in 96 ms + evt echo *14*1*06##)
WHO=14 Shutter Actuator Lock (BT-F411/2 5 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) — BTicino F461 (FW 2.0.11) + BT-F411/2 shutter actuator (WHERE 05, 5 s travel time) *14*0*05## (cmd ACK in 105 ms + evt echo *14*0*05## + automatic hardware *2*0*05## STOP 54 ms later; blocks *2*1*05## UP and *2*2*05## DOWN while locked), *14*1*05## (cmd ACK in 111 ms + evt echo *14*1*05##; restores full *2*1000#0*05## $\to$ *2*0*05## $\to$ *2*1000#1/2*05## $\to$ *2*1/2*05## $\to$ 5 s auto-stop *2*0*05## cycle)
WHO=14 Interface Routing (#4#01), Unpopulated Timeout & Parser Boundaries f461_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) — BTicino F461 (FW 2.0.11) *14*0*07#4#01## (cmd ACK in 108 ms), *14*1*07#4#01## (cmd ACK in 101 ms), unpopulated *14*0*99## / *14*1*99## (cmd NACK after 2,053 ms / 2,052 ms bus timeout, 0 monitor frames), status query *#14*07## (cmd NACK in 53 ms), and invalid WHAT=2 *14*2*07## (cmd NACK in 50 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 from own-firmware-oracle/results/<PRODUCT>/<VERSION>/oracle/full/special-who14.tsv):

  • WHO=14 Special 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

  1. Grammar & Meaning (OpenWebNet-Encyclopedia + OPEN.db + ScenarioDevices + bt_luci / coso Binary Analysis):
    • OPEN.db & ScenarioDevices (ownkb:claim:c000883..c000886, ownkb:claim:c000899..c000902): OPEN.db defines IDWHO = 14 as "Special commands". MyHOME_Suite ScenarioDevices (3.5.38) binds miniScenarioSuite.specialCommands.actionLockUnlockActuator.lock to *14*0*WHERE## (WHAT = 0, lock/disable actuator) and .unlock to *14*1*WHERE## (WHAT = 1, unlock/enable actuator).
    • Firmware Scanner & SCS Bus Bridge (ownkb:claim:c002015..c002022): In bt_luci (F453 010100, F454 020005, MH200N 010108, F461 020011) and coso (F450 020018, H4684 060004), flex scanner rules 201–203 ("Messaggio openwebnet comando spec") accept only WHAT = 0 or WHAT = 1 with Point-to-Point WHERE (11–99, 01–09, 0110–0915, 1010–1015) and optional #4#01..09 local bus interface routing (*14*[01]*WHERE(#4#0[1-9])?##), and rules 232–234, 341, 362 ("Messaggio scs illuminazione comando speciale") translate symmetrically between OpenWebNet and SCS opcodes 001100 (WHAT=0 lock) and 001000 (WHAT=1 unlock). Status requests (*#14*WHERE##) have no flex rule and are immediately NACKed.
  2. Firmware Oracle Verdicts (own-firmware-oracle) & Live F461 Hardware Verification (MyHOME#752):
Gateway Target Daemon *14*0*WHERE## (Lock) Reply / Verdict *14*1*WHERE## (Unlock) Reply / Verdict Emitted SCS Bus Telegrams (Lock / Unlock) Notes
F450 (020018), H4684 (060004) coso NACK / OUT NACK / OUT $0332001100\r / $0332001000\r Legacy coso parser forwards onto SCS bus
F453 (010100), F453AV (010035), MH200 (020100), MH200N (010108) bt_luci ACK / OUT ACK / OUT $0332001100\r / $0332001000\r Immediate ACK + standard 7-byte SCS telegram
F454 (020005), F459 (010019), MH202 (020613) bt_luci ACK / OUT ACK / OUT $00332001100\r / $00332001000\r Immediate ACK + extended $003 SCS telegram
F460 (020007), F461 (020011), MyHomeServer1 (023809) (unpopulated WHERE 32 / 99) bt_luci NACK / OUT NACK / OUT $00332001100\r / $00332001000\r Waits ~2.05 s for physical actuator bus ACK; returns *#*0## when unpopulated
Live F461 (020011) + Physical BT-F411/4 (06, 07) & BT-F411/2 (05) (MyHOME#752) bt_luci ACK (89–108 ms) + evt echo ACK (91–111 ms) + evt echo $003<addr>001100\r / $003<addr>001000\r ($04<addr>01001100\r on #4#01) Physical actuator ACKs on SCS bus; F461 returns *#*1## on cmd and echoes *14*0/1*WHERE## on evt

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@GreenGrassBlueOcean GreenGrassBlueOcean changed the title feat(who14): implement WHO 14 actuator lock/unlock and live F461/F411 golden fixtures feat(who14): implement WHO 14 actuator lock and unlock Oct 18, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants