Skip to content

feat(message): WHO 1004 thermoregulation diagnostics, WHO 16 tuner steps and station store, WHO 22 seek and step builders (companion of MyHOME#839) - #153

Merged
GreenGrassBlueOcean merged 4 commits into
OpenWebNet-HA:masterfrom
gdluck:feat/who1004-diagnostics-and-tuner-steps
Nov 2, 2026

Conversation

@gdluck

@gdluck gdluck commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Requested by A

Summary

Before: OWNd has no WHO 1004 (thermoregulation autodiagnostics) classes, no WHO 16 FM tuner step or station-store builders, and no WHO 22 manual-step builders; a WHO 22 dimension 11 report with preset 0 is indistinguishable from one without a preset field.

After: OWNHeatingDiagnosticEvent / OWNHeatingDiagnosticCommand decode and request WHO 1004 diagnostics with the libwhere.so / libdiag.so grammar; OWNSoundCommand.step_frequency_up / step_frequency_down (WHAT 5001..5015 / 5101..5115) and store_station (1..5) build the WHO 16 frames the firmware ACKs; OWNSoundDiffusionCommand.seek_up / seek_down (bare WHAT 5 / 6) and step_frequency_up / step_frequency_down (5#STEP / 6#STEP, 1..15) build the WHO 22 frames; OWNSoundDiffusionEvent exposes frequency_step, the seek / step flags and is_preset_cleared.

Context. Companion of OpenWebNet-HA/MyHOME#839 (andrea-mag's H/L4684 6.0.11 captures on OpenWebNet-HA/MyHOME#466), for 2.0.0b13; the CHANGELOG bullets sit under a new [Unreleased] / Added above the shipped 2.0.0b12 section. Revision 3b76ab1 (on master 360e4f4, after #152 merged) implements @GreenGrassBlueOcean's corrected, firmware-verified specification (review 5738866331, findings F1..F7), replacing the first spec from #152; #152 ships 2.0.0b12 without it.

Changes

  1. OWNd/message/heating.py (F1, F2, F3): OWNHeatingDiagnosticEvent scopes per OPN_ThermoWhereTransl: #0 / #0#C -> central; #Z / #Z#C -> zone (zone_number); Z 1..99 -> probe; NZZ -> slave_probe (zone_number, probe_number); [1-9]00 -> external_probe; Z#N -> actuator (zone_number, actuator_number); anything else (0, 901, 66#0, #0#0, #66#1#2) -> None. unique_id is 1004-<full WHERE> on both classes (1004-#0#2, 1004-#66#2, 1004-66#1), so central-unit selectors and actuator numbers no longer collide; 1004 stays out of OWNMessage.interface. diagnostic_mask: 24-bit 0 / 1 string for dimensions 7, 8, 11, 12 on central and the addressed devices, 16-bit for dimensions 21 and 22 on zone, exactly one value, else None; cleared_bits 1-based from the left, has_fault; the docstring notes the WHO_4 v2.0.0 pp. 69-74 active-low bit tables while keeping the mask raw. Dimension 23 on central with two ASCII-digit values -> non_responding_units, faulty_units. slave_count, scalar faulty_zone and the dimension 13 "local offset" claim are gone. Builders, only the keys libdiag.so registers: request_central_diagnostic(where="#0", *, dimension=7) (7 / 8 / 11 / 12, _validate_central_where), request_device_diagnostic(where, *, dimension=7) (Z, NZZ, [1-9]00, Z#N), request_zones_with_failures (*20##), request_zone_diagnostic (*21##; #0, #0#C, 1..99 -> #Z, #Z, #Z#C), request_fault_counts (*23##). No *22## request (R22P0 does not exist). Every integer validator rejects bool and non-int / str before int(). The is_who4_dimension_22 / is_who4_dimension_22_crash_form negative guards for *#1004*#66*22## stay.
  2. OWNd/message/sound.py, WHO 16 (F4, F5): step_frequency_up -> *16*{5000+s}*W##, step_frequency_down -> *16*{5100+s}*W## (1..15); OWNSoundEvent.frequency_step_forward / frequency_step_backward from 5001..5015 / 5101..5115 (4001 / 4101 no longer decoded); store_station restricted to 1..5 with the note that 6..15 need WHO 22 store_preset; the dimension 10 event decode is gone (bt_difson never emits it).
  3. OWNd/message/sound.py, WHO 22 (F6, F7a): step_frequency_up(where="6", step=1) -> *22*5#{s}*W##, step_frequency_down -> *22*6#{s}*W## (1..15); WHAT 7 / 8 removed; frequency_step (1..15, else None) with is_step_frequency_up / is_step_frequency_down on 5#STEP / 6#STEP (a step of 0, 16 or 01 sets no flag); is_seek_up / is_seek_down for bare WHAT 5 / 6; is_preset_cleared kept as defensive parsing of *#22*…*11*1*<freq>*0##, without the F500-trace attribution.
  4. Citations (F7b): the three kb_who_* slugs are replaced everywhere by the ownkb:* ids of the review. scripts/sync_documentation.py / README.md: the 4 / 1004 catalogue row describes the corrected scopes and dimensions. CHANGELOG.md: three bullets under [Unreleased] / Added.

State Diagram

stateDiagram-v2
    direction TB
    state "WHO 1004 WHERE (OPN_ThermoWhereTransl) -> scope" as Scope {
        [*] --> Central : #0, #0#C (dims 7 8 11 12 -> 24 bits, 20 request, 23 -> two counts)
        [*] --> Zone : #Z, #Z#C (dims 21, 22 -> 16 bits)
        [*] --> Probe : Z 1..99 (24 bits)
        [*] --> SlaveProbe : NZZ, N 1..8 (24 bits, probe_number)
        [*] --> ExternalProbe : 100..900 (24 bits)
        [*] --> Actuator : Z#N, N 1..9 (24 bits, actuator_number)
        [*] --> Unknown : 0, 901, 66#0, #0#0, #66#1#2 -> scope None, mask None
    }
    state "WHO 16 / WHO 22 tuner (golden oracle rows)" as Tuner {
        [*] --> W16Step : *16*5001..5015*W## / *16*5101..5115*W## ack on 6 gateways
        [*] --> W16Store : *#16*W*#10*1..5## ack on 6 gateways
        [*] --> W22Seek : *22*5*W## / *22*6*W## ack on 8 gateways
        [*] --> W22Step : *22*5#1..15*W## / *22*6#1..15*W## ack on 8 gateways
    }
Loading

Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 1004 OpenWebNet-HA/MyHOME#839 tests/fixtures/traces/issue_466/myhome_trace_HL4684_all_2026-11-01T11-30-19.json H/L4684 6.0.11 + 3550 central unit + F430/4, zone 66 *#1004*66*11*111111111111111111111111## (probe), *#1004*#66*22*1111111111111111## (zone), *#1004*#0*11*111111111111111111111111## (central), verbatim in the tests
WHO 16 / WHO 22 F500 tuner same PR, ...T11-48-15.json H/L4684 6.0.11 + F500 *#16*101*6*0*105300##, *#16*101*7*0*5##, *#22*5#2#1*5*1*10530##, *#22*5#2#1*11*1*10530*5##. The off-preset *0## shape is not in the trace and is tested as a spec-shaped frame only.

What does the Oracle say

tests/golden/firmware_oracle.json, pinned by the tests:

Frame Result
*16*5001*101##, *16*5101*101## ack / out $0494018FA1 / $0494018FB1 on F453, F453AV, F454, H4684, MH200, MH200N; nack / silent on the other six. The old 4001 / 4101 spellings are asserted absent.
*#16*101*#10*5## ack / out $0493018FD4 on the same six
*22*5*2#1##, *22*6*2#1##, *22*5#1*2#1##, *22*6#1*2#1## ack / out $0494018FA0 / FB0 / FA1 / FB1 on F453, F453AV, F454, F459, F460, F461, MH200N, MyHomeServer1; MH202 nack / out; F450, H4684, MH200 nack / silent
any WHO 1004 frame not replayed yet; the decoders rest on the physical frames above and on libdiag.so as the review documents

Testing

  • tests/test_who1004_heating_diagnostic.py: every scope on the physical frames and on edge WHEREs (0, 100, 901, 66#0, #0#0, #66#1#2, 10#1, 166, 800), the per-scope mask dimensions (a 16-bit mask on a probe or a 24-bit mask on a zone gives None), cleared_bits / has_fault, dimension 23 counts, unique_id with every WHERE form, all five builders with rejected inputs (bool, float, out of range, wrong scope), the dimension 22 guard regressions, OWNEvent.parse == OWNMessage.parse.
  • tests/test_sound.py: the 5001 / 5101 steps and the 1..5 store with rejected inputs, the event decode, and the golden-row pins above. tests/test_sound_diffusion.py: the 5#STEP / 6#STEP builders and decode, frequency_step edges (0, 16, 01), bare seeks, is_preset_cleared, the four oracle rows in the WHO 22 conformance parametrisation plus a positive pin on all eight ack gateways.
  • Full suite: 2786 passed; scripts/verify_coverage.py 6,986 / 6,986 statements; branches 100 % on heating.py, sound.py adds no partial branch (its 11 pre-exist on master); ruff check, mypy OWNd, scripts/verify_library_standards.py and scripts/sync_documentation.py --check pass.

Judgement calls

  • is_step_frequency_up / is_step_frequency_down are True only with a valid step 1..15 (5#0 aliases the seek on bt_multi, 5#16 overflows; neither sets a flag).
  • request_central_diagnostic defaults to dimension 7 (the review's ownkb:chunk:r000205); the first revision defaulted to 11.
  • The 4 / 1004 catalogue row text was rewritten because its old wording contradicted the corrected scopes; row structure and dispatch keys are unchanged.
  • First-listed names used wherever the review offered two (zone, slave_probe, request_device_diagnostic).

🤖 Generated with Claude Code

@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!

gdluck pushed a commit to gdluck/OWNd that referenced this pull request Nov 1, 2026
… 22 seek builders ship in 2.0.0b12

The three companion bullets (OpenWebNet-HA#153) move under
[2.0.0b12] - 2026-11-01, as the release carries them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @gdluck — thank you for putting this PR together so cleanly and thoroughly, and huge credit for flagging the three oracle/trace discrepancies in your PR body:

  1. *16*4001*101##, *16*4101*101##, *22*7*2#1##, *22*8*2#1##, and the cited firmware versions being absent from firmware_oracle.json, while noting that the functional docs list *22*5#STEP*2#S## / *22*6#STEP*2#S##;
  2. *#16*101*#10*5## emitting $0493018FD4 ($0494018FA0 being *22*5*2#1##); and
  3. *#22*5#2#1*11*1*10530*0## not appearing in myhome_trace_HL4684_all_2026-11-01T11-48-15.json.

Every single one of those catches was right on the money. I audited the implementation and my earlier specification comment (#152 (comment)) directly against libdiag.so, libwhere.so, bt_difson, and bt_multi disassemblies, tests/golden/firmware_oracle.json, and the OpenWebNet-Encyclopedia Machine KB (ownkb-build-0.9.0). All of the protocol defects below originated in my #152 spec comment — my apologies for handing you an unverified spec.

Let's reset #152 back to 76b9550c so 2.0.0b12 ships cleanly right away, and rework #153 on top of master for 2.0.0b13 using the verified specification below. (OWNd/message/__init__.py, scripts/sync_documentation.py, WHO 22 seek_up/seek_down + oracle rows, and the is_who4_dimension_22 negative guards are already clean and reusable as-is.)


1. Summary of Protocol Findings (F1–F7)

ID Severity Location Finding & Primary-Source Proof
F1 Critical heating.py (request_zone_actuator_diagnostic) Emits *#1004*#Z*22##. Across libdiag.so (F453, F454, F450, MH200N, MH202, MyHomeServer1, F460, F461), OPN_OpenScsDiagTermoTr registers only R20P0, R21P0, and R23P0 on top of base R0P0..R12P0. R22P0 does not exist ("Termo Object command not Found for key: R22P0" -> *#*0## NACK); DIMENSION 22 is spontaneous s2o-only (ownkb:chunk:r000205, "No request defined in this flow"). Single-zone queries use *#1004*#Z*21## (R21P0).
F2 Critical heating.py (OWNHeatingDiagnosticEvent) Wrong wire semantics for DIM 7, 13, 20, 21, 23:
• DIM 7 (s2o_diagnosticsA, ownkb:claim:c004423): 24-bit '0'/'1' diagnostic mask on #0/#0#C and addressed devices (mirroring OWNLightingDiagnosticEvent _MASK_DIMENSIONS = (7, 11)), never 0..8 slave_count (DIM 8 & DIM 12 are also 24-bit masks).
• DIM 21 (s2o_autoDiagnostics in libdiag.so): zone == 0 is rejected ('Invalid zone value', so #0*21*0 is never emitted); 1 <= zone <= 99 jumps to the exact same 16-bit '0'/'1' mask formatter as DIM 22, emitting *#1004*#ZONE*21*BIT16## on #Z / #Z#C (currently fails closed to None).
• DIM 23 (s2o_autoDiagnostics zone == 100, ownkb:claim:c004442): emitted on #0 / #0#C as two decimal integer counts *#1004*#0*23*NO_RESPONSE*FAULTS##, not per-actuator 1..9 on #Z.
• DIM 20 (R20P0) is a request-only trigger returning zero or more *#1004*#ZONE*21*BIT16## frames; DIM 13 in OPN_OpenScsDiagIdTr is a generic 32-bit hex Device ID, not WHO 4 local offset.
F3 Medium heating.py (WHERE & unique_id) OPN_ThermoWhereTransl (libwhere.so) accepts #0/#0#C, #Z/#Z#C (zone Z under 4-zone central unit C), Z (1..99 master probes), NZZ (101..899 slave probes, N=1..8, ZZ=01..99), [1-9]00 (100..900 external probes), and Z#N (Z=1..99, N=1..9 zone actuators such as 66#1). Also, because 1004 is (rightly) excluded from OWNMessage.interface, inherited OWNMessage.unique_id drops _where_param, colliding #0/#0#1/#0#2, #66/#66#2, and 66/66#1.
F4 Critical sound.py (WHO 16 step_frequency_*) Emits/decodes WHAT 4001..4015 / 4101..4115, which bt_difson NACKs on every WHO 16 gateway. Per ownkb:chunk:r000371 (ownkb:claim:c002096, c002098) and bt_difson o2s, manual frequency steps are 5001..5015 / 5101..5115 (5000/5100 = auto-seek) — and *16*5001*101## / *16*5101*101## are already in tests/golden/firmware_oracle.json (ack / out $0494018FA1 / $0494018FB1 across F453, F453AV, F454, H4684, MH200, MH200N).
F5 High sound.py (WHO 16 store_station & DIM 10) OWNSoundCommand.store_station allows 6..15, which bt_difson (cmp r0, #5; bgt NACK) NACKs on every WHO 16 gateway (ownkb:claim:c002128: "For DIMENSION 10, Established detail is station range 1 through 5"; 6..15 requires WHO 22 OWNSoundDiffusionCommand.store_preset *22*33#P*2#S##). And bt_difson s2o only formats dimensions 1, 6, 7, 8, 11 — never DIMENSION 10 (ownkb:claim:c002135), and *#16*101*10## is NACKed.
F6 High sound.py (WHO 22 step_frequency_*) Emits/decodes WHAT 7 / WHAT 8, which bt_multi logs as "Increment/Decrement subSource - not implemented yet" and NACKs (*#*0##), while leaving real manual steps *22*5#STEP*2#S## / *22*6#STEP*2#S## (1..15, ownkb:claim:c002729) with is_step_frequency_up == False. *22*5#1*2#1## and *22*6#1*2#1## are already in tests/golden/firmware_oracle.json (ack / out $0494018FA1 / $0494018FB1 across all 8 WHO 22 gateways).
F7 Medium-High sound.py, CHANGELOG.md, tests (a) sound.py docstrings/comments and CHANGELOG.md attribute *#22*5#2#1*11*1*10530*0## to (H/L4684 6.0.11 F500 trace, MyHOME#839) (bt_multi emits DIM 5 and skips DIM 11 when SCS station nibble is 'F', and *0## is absent from that trace).
(b) kb_who_04_thermoregulation_diagnostics, kb_who_16_what_frequency_step, and kb_who_16_dim_station (13 occurrences across 5 files) do not exist in the Machine KB (ownkb:* IDs listed below).
flowchart LR
    subgraph WHO16["WHO 16 (bt_difson)"]
        W16Step["*16*5001..5015*101## / *16*5101..5115*101##"] -->|"ACK + bus"| BusStep["$0494018FA1..FAF / FB1..FBF"]
        W16Store["*#16*101*#10*1..5## (write-only)"] -->|"ACK + bus"| BusStore["$0493018FD0..FD4"]
    end
    subgraph WHO22["WHO 22 (bt_multi)"]
        W22Seek["*22*5*2#1## / *22*6*2#1##"] -->|"ACK + bus"| BusSeek["$0494018FA0 / FB0"]
        W22Step["*22*5#1..15*2#1## / *22*6#1..15*2#1##"] -->|"ACK + bus"| BusStep
        W22Store["*22*33#1..15*2#1##"] -->|"ACK + bus"| BusStore22["$0493018FD0..FDE"]
    end
    subgraph WHO1004["WHO 1004 (libdiag.so)"]
        D7["*#1004*WHERE*7## (R7P0)"] -->|"24-bit mask"| E7["*#1004*WHERE*7*BIT24##"]
        D20["*#1004*#0*20## (R20P0)"] -->|"0..N faulty zones"| E21["*#1004*#Z*21*BIT16##"]
        D21["*#1004*#Z*21## or #0*21## (R21P0)"] -->|"16-bit mask"| E21
        D22["Spontaneous s2o only (no R22P0)"] -->|"16-bit mask"| E22["*#1004*#Z*22*BIT16##"]
        D23["*#1004*#0*23## (R23P0)"] -->|"2 decimal counts"| E23["*#1004*#0*23*NO_RESPONSE*FAULTS##"]
    end
Loading

2. Corrected Specification for #153

(Note across all new integer validators in heating.py and sound.py: guard if isinstance(val, bool) or not isinstance(val, (int, str)): raise ValueError(...) so True / False / 1.9 never silently coerce via int().)

A. WHO 1004 Thermoregulation Autodiagnostics (OWNd/message/heating.py + OWNd/message/__init__.py)

  • Verified Machine KB citations: ownkb:chunk:r000204–r000209, ownkb:claim:c004423, ownkb:claim:c004429–c004448 (WHO_4 v2.0.0 pp. 69–74), plus libdiag.so (OPN_OpenScsDiagGenTr, OPN_OpenScsDiagTermoTr) and libwhere.so (OPN_ThermoWhereTransl).
  • WHERE grammar & scopes (OPN_ThermoWhereTransl):
    1. Central unit (#0 or #0#<1..99>, _WHO4_CENTRAL_WHERE_REGEX) -> scope == "central", zone_number is None.
    2. Central-unit zone (#<1..99> or #<1..99>#<1..99> — zone Z on central unit / 4-zone central unit C, matching heating.py:116-120) -> scope == "zone" (or "zone_actuators"), zone_number == Z.
    3. Addressed bus devices:
      • Master probe 1..99 (Z) -> scope == "probe", zone_number == Z.
      • Slave probe NZZ (N = 1..8, ZZ = 01..99, i.e. 101..199, 201..299, …, 801..899) -> scope == "slave_probe" (or "probe"), zone_number == ZZ, probe_number == N.
      • External probe [1-9]00 (100, 200, …, 900) -> scope == "external_probe", zone_number is None.
      • Zone actuator Z#N (Z = 1..99, N = 1..9, e.g. 66#1) -> scope == "actuator", zone_number == Z, actuator_number == N.
      • Anything else (0, 901, 66#0, #0#0, #66#1#2, etc.) -> scope is None, zone_number is None.
  • unique_id collision fix:
    • Keep 1004 excluded from OWNMessage.interface.
    • Override unique_id on both OWNHeatingDiagnosticEvent and OWNHeatingDiagnosticCommand using the full reconstructed WHERE (#0#2 -> "1004-#0#2", #66#2 -> "1004-#66#2", 66#1 -> "1004-66#1") so _where_param is never dropped.
  • OWNHeatingDiagnosticEvent properties (fail-closed):
    • diagnostic_mask: str | None:
      • 24-bit '0'/'1' mask when dimension in (7, 8, 11, 12) and scope in ("central", "probe", "slave_probe", "external_probe", "actuator"), len(dimension_value) == 1, len(v) == 24, set(v) <= {"0", "1"}.
      • 16-bit '0'/'1' mask when dimension in (21, 22) and scope == "zone" (or "zone_actuators"), len(dimension_value) == 1, len(v) == 16, set(v) <= {"0", "1"}.
      • Otherwise None.
    • cleared_bits: tuple[int, ...] | None (1-based positions from left where bit == '0') and has_fault: bool | None (bool(cleared_bits) when diagnostic_mask is not None, else None). Note in docstring (ownkb:chunk:r000206–r000207) that WHO_4 v2.0.0 pp. 69–74 documents active-low bit conditions (13..16, 21..24 for 24-bit central masks; 11..16 for 16-bit zone masks) while keeping diagnostic_mask raw until hardware fault injection confirms bit-order indexing on the bus.
    • DIMENSION 23 fault-count summary on scope == "central" (*#1004*#0*23*NO_RESPONSE*FAULTS##, ownkb:claim:c004442): when dimension == 23, scope == "central", len(dimension_value) == 2, and both values are ASCII digits, expose non_responding_units: int | None and faulty_units: int | None (else None).
    • Remove slave_count, scalar faulty_zone, and DIM 13 local-offset claims.
  • OWNHeatingDiagnosticCommand builders (only keys registered in libdiag.so: R7P0, R8P0, R11P0, R12P0, R20P0, R21P0, R23P0 — remove request_zone_actuator_diagnostic / *22##):
    • request_central_diagnostic(cls, where: str = "#0", *, dimension: int = 7) -> OWNHeatingDiagnosticCommand -> *#1004*{target}*{dimension}## (dimension in (7, 8, 11, 12), defaulting to 7 per ownkb:chunk:r000205, validated with _validate_central_where).
    • request_device_diagnostic(cls, where: int | str, *, dimension: int = 7) -> OWNHeatingDiagnosticCommand (or request_probe_diagnostic, dimension in (7, 8, 11, 12) on valid addressed bus devices Z, NZZ, [1-9]00, Z#N).
    • request_zones_with_failures(cls, where: str = "#0") -> OWNHeatingDiagnosticCommand -> *#1004*{target}*20## (R20P0, _validate_central_where).
    • request_zone_diagnostic(cls, where: int | str = "#0") -> OWNHeatingDiagnosticCommand -> *#1004*{target}*21## (R21P0: accepts #0, #0#C, 1..99 normalized to #Z, #Z, or #Z#C).
    • request_fault_counts(cls, where: str = "#0") -> OWNHeatingDiagnosticCommand -> *#1004*{target}*23## (R23P0, _validate_central_where).

B. WHO 16 FM Tuner Steps & Preset Store (OWNd/message/sound.py)

  • Verified citations: ownkb:chunk:r000371, ownkb:chunk:r000376, ownkb:claim:c002096, ownkb:claim:c002098, ownkb:claim:c002128, ownkb:claim:c002135, plus tests/golden/firmware_oracle.json (sound-who16-source).
  • Frequency steps (5001..5015 / 5101..5115):
    • OWNSoundCommand.step_frequency_up(cls, where: str | int, step: int = 1) -> *16*{5000 + s}*{target}## (1 <= s <= 15).
    • OWNSoundCommand.step_frequency_down(cls, where: str | int, step: int = 1) -> *16*{5100 + s}*{target}## (1 <= s <= 15).
    • OWNSoundEvent: decode 5001 <= self._state <= 5015 -> frequency_step_forward = self._state - 5000 and 5101 <= self._state <= 5115 -> frequency_step_backward = self._state - 5100.
    • Pin *16*5001*101## and *16*5101*101## positively against tests/golden/firmware_oracle.json (ack / out $0494018FA1 and $0494018FB1 on F453, F453AV, F454, H4684, MH200, MH200N).
  • Station store (1..5 write-only):
    • Restrict OWNSoundCommand.store_station(cls, where: str | int, station: int) to 1 <= station <= 5 (ownkb:claim:c002128, bt_difson ceiling), noting in the docstring that presets 6..15 require WHO 22 OWNSoundDiffusionCommand.store_preset (*22*33#<P>*2#<S>##).
    • Revert OWNSoundEvent back to elif self._dimension == 7: (DIMENSION 10 is write-only and never emitted by bt_difson). (We also cleaned up the matching downstream MyHOME check in OpenWebNet-HA/MyHOME#844.)

C. WHO 22 Seek, Frequency Steps & is_preset_cleared (OWNd/message/sound.py)

  • Verified citations: ownkb:chunk:r000478–r000480, ownkb:claim:c002729, plus tests/golden/firmware_oracle.json (sound-who22).
  • Keep OWNSoundDiffusionCommand.seek_up (*22*5*{target}##) and seek_down (*22*6*{target}##) + is_seek_up / is_seek_down (bare WHAT 5 / 6 with no parameter) as-is.
  • Manual frequency steps (*22*5#<1..15>*W## / *22*6#<1..15>*W##):
    • Change OWNSoundDiffusionCommand.step_frequency_up(cls, where: str | int = "6", step: int = 1) -> *22*5#{s}*{target}## and step_frequency_down(cls, where: str | int = "6", step: int = 1) -> *22*6#{s}*{target}##, validating 1 <= s <= 15 (bt_multi 5#0 aliases seek FA0, >=16 overflows %.1X).
    • Add *22*5#1*2#1## and *22*6#1*2#1## to the WHO 22 oracle conformance parametrisation (ack / out $0494018FA1 / $0494018FB1 across all 8 WHO 22 gateways in tests/golden/firmware_oracle.json).
    • Remove WHAT 7 / WHAT 8 from OWNSoundDiffusionEvent and wire is_step_frequency_up / is_step_frequency_down + frequency_step: int | None (1..15, failing closed outside 1..15) to WHAT 5#<step> / 6#<step>.
  • is_preset_cleared & KB slugs:
    • Keep is_preset_cleared as defensive parsing for explicit *#22*...*11*1*<freq>*0## frames, removing the (H/L4684 6.0.11 F500 trace, MyHOME#839) attribution from sound.py and CHANGELOG.md.
    • Replace all kb_who_* slugs across CHANGELOG.md, heating.py, sound.py, and tests with the verified ownkb:* IDs above.

gdluck and others added 4 commits November 2, 2026 18:00
…mmand

Add OWNHeatingDiagnosticEvent and OWNHeatingDiagnosticCommand for WHO 1004,
the diagnostic WHO of the temperature control system (Encyclopedia Machine
KB ownkb:chunk:r000204-r000209, ownkb:claim:c004423, c004429-c004448;
libdiag.so OPN_OpenScsDiagGenTr / OPN_OpenScsDiagTermoTr, libwhere.so
OPN_ThermoWhereTransl), registered for WHO 1004 and re-exported from
OWNd.message.

The event follows the OPN_ThermoWhereTransl WHERE grammar: #0 / #0#C is
the central unit (scope "central"), #Z / #Z#C a zone through the central
unit ("zone", zone_number), Z a master probe ("probe"), NZZ a slave probe
("slave_probe", zone_number ZZ, probe_number N), [1-9]00 an external probe
("external_probe"), Z#N a zone actuator ("actuator", actuator_number);
anything else has no scope. diagnostic_mask is the 24-bit mask of
DIMENSIONS 7, 8, 11 and 12 on the central unit and the addressed devices
or the 16-bit mask of DIMENSIONS 21 and 22 on a zone, only when the frame
carries exactly one all-0/1 value of that length (cleared_bits, has_fault
follow it; the mask stays raw, WHO_4 v2.0.0 pp. 69-74 bit conditions are
noted but not indexed). DIMENSION 23 on the central unit is decoded into
non_responding_units and faulty_units when it carries two decimal counts.
Every other shape fails closed with the raw values in dimension_value.

unique_id is overridden on both classes with the full WHERE (1004-#0#2,
1004-OpenWebNet-HA#66#2, 1004-66#1): WHO 1004 stays out of OWNMessage.interface, so
the inherited id would have dropped the WHERE parameters.

The builders emit only the keys libdiag.so registers (R7P0, R8P0, R11P0,
R12P0, R20P0, R21P0, R23P0): request_central_diagnostic (*#1004*#0*7##,
dimension 7 / 8 / 11 / 12), request_device_diagnostic (Z, NZZ, [1-9]00,
Z#N), request_zones_with_failures (*20##), request_zone_diagnostic
(*21##; #0, #0#C, 1..99 as #Z, #Z, #Z#C) and request_fault_counts
(*23##). There is no *22## request. Integer and address validators refuse
bool and non int / str values before int().

The physical frames are the first WHO 1004 frames captured on a bus,
pushed by an H/L4684 6.0.11 with a 3550 central unit once the F430/4
channel 66#1 came back online (MyHOME#839, fixture issue_466
myhome_trace_HL4684_all_2026-11-01T11-30-19.json):
*#1004*66*11*111111111111111111111111##, *#1004*OpenWebNet-HA#66*22*1111111111111111##
and *#1004*#0*11*111111111111111111111111##. No WHO 1004 frame is in the
firmware oracle index. The WHO 4 DIMENSION 22 guard anchors on WHO 4, so
*#1004*OpenWebNet-HA#66*22*...## is not a crash form; the tests pin that. The README
WHO catalog lists WHO 1004 under WHO 4.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add OWNSoundCommand.step_frequency_up / step_frequency_down (WHAT
5001..5015 / 5101..5115, step 1..15; Encyclopedia Machine KB
ownkb:chunk:r000371, ownkb:claim:c002096 / c002098; 5000 / 5100 stay the
automatic seeks of seek_up / seek_down) and OWNSoundCommand.store_station
(*OpenWebNet-HA#16*WHERE*OpenWebNet-HA#10*STATION##, station 1..5: ownkb:chunk:r000376,
ownkb:claim:c002128, the bt_difson ceiling; stations 6..15 of an F500N go
through WHO 22 store_preset). An out-of-range step or station, a bool or
non int / str value and an empty WHERE raise ValueError.

OWNSoundEvent decodes WHAT 5001..5015 into frequency_step_forward and
5101..5115 into frequency_step_backward (1..15, as track_step_forward /
track_step_backward do for 6001..6115). DIMENSION 10 is write-only
(bt_difson never formats it, ownkb:claim:c002135), so a DIMENSION 10 value
frame keeps nothing decoded; only DIMENSION 7 reads into track.

Evidence: *16*5001*101## and *16*5101*101## are ACKed and put on the bus
($0494018FA1 / $0494018FB1) by F453, F453AV, F454, H4684, MH200 and MH200N
in the firmware oracle (sound-who16-source; F450, F459, F460, F461, MH202
and MyHomeServer1 NACK them), as is *OpenWebNet-HA#16*101*OpenWebNet-HA#10*5## ($0493018FD4) on the
same gateways; the tests pin all three rows. The physical F500 frames
*OpenWebNet-HA#16*101*6*0*105300## and *OpenWebNet-HA#16*101*7*0*5## (105.30 MHz, station 5) come
from an H/L4684 6.0.11 (MyHOME#839, fixture issue_466
myhome_trace_HL4684_all_2026-11-01T11-48-15.json).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add OWNSoundDiffusionCommand.seek_up (*22*5*WHERE##), seek_down
(*22*6*WHERE##), step_frequency_up (*22*5#STEP*WHERE##) and
step_frequency_down (*22*6#STEP*WHERE##), STEP 1..15 (Encyclopedia
Machine KB ownkb:chunk:r000478-r000480, ownkb:claim:c002729), WHERE
defaulting to "6" like next_station / previous_station; a step outside
1..15 (5#0 aliases the seek on bt_multi, 16 and above overflow its
one-nibble step), a bool or non int / str step and an empty WHERE raise
ValueError. WHAT 7 / 8 are not emitted: bt_multi NACKs them as
"Increment/Decrement subSource - not implemented yet".

OWNSoundDiffusionEvent gains is_seek_up / is_seek_down for a bare WHAT 5
/ 6, frequency_step (1..15, None outside that range) with
is_step_frequency_up / is_step_frequency_down for WHAT 5#STEP / 6#STEP,
human-readable log lines for all four, and is_preset_cleared: True for a
DIMENSION 11 report whose frequency decoded and whose preset field is an
explicit 0, defensive parsing of the spec-shaped frame. preset stays None
there, as it is for a frame without a preset field (DIMENSION 5), so the
flag is what tells the two apart.

Evidence: *22*5*2#1##, *22*6*2#1##, *22*5#1*2#1## and *22*6#1*2#1## are
ACKed and put on the bus ($0494018FA0 / FB0 / FA1 / FB1) by F453, F453AV,
F454, F459, F460, F461, MH200N and MyHomeServer1 in the firmware oracle
(sound-who22; F450, H4684 and MH200 NACK them, MH202 NACKs but forwards)
and join the WHO 22 oracle conformance rows; *22*7*2#1## and *22*8*2#1##
are not in the oracle index, and the tests pin that. The physical F500
frames *OpenWebNet-HA#22*5#2#1*5*1*10530## and *OpenWebNet-HA#22*5#2#1*11*1*10530*5## (105.30 MHz,
station 5) come from an H/L4684 6.0.11 (MyHOME#839, fixture issue_466
myhome_trace_HL4684_all_2026-11-01T11-48-15.json).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… seek and step builders

Add the Unreleased / Added bullets for OWNHeatingDiagnosticEvent and
OWNHeatingDiagnosticCommand (WHO 1004: OPN_ThermoWhereTransl scopes,
24-bit and 16-bit masks, Dimension 23 fault counts, the libdiag.so
request keys, full-WHERE unique_id), the WHO 16 step_frequency_up /
step_frequency_down (WHAT 5001..5015 / 5101..5115) and store_station
(1..5) builders and event decoding, and the WHO 22 seek_up / seek_down /
step_frequency_up / step_frequency_down (5#STEP / 6#STEP) builders, the
seek and step flags, frequency_step and is_preset_cleared, with the
Encyclopedia ownkb:* ids, the firmware oracle rows and the H/L4684 6.0.11
traces (MyHOME#839) they rest on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the feat/who1004-diagnostics-and-tuner-steps branch from cc2f37e to 3b76ab1 Compare November 2, 2026 16:01
@gdluck

gdluck commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Reworked to the corrected specification (review 5738866331), revision 3b76ab1: four commits on master 360e4f4 (after #152 merged), CHANGELOG bullets under a new [Unreleased] / Added, for 2.0.0b13.

  • F1 / F2 / F3: OPN_ThermoWhereTransl scopes (central, zone, probe, slave_probe + probe_number, external_probe, actuator + actuator_number, else None); unique_id = 1004-<full WHERE> on both classes; 24-bit masks on dimensions 7 / 8 / 11 / 12 for the central unit and addressed devices, 16-bit on 21 / 22 for a zone; dimension 23 -> non_responding_units / faulty_units; slave_count, scalar faulty_zone and the dimension 13 claim removed; builders request_central_diagnostic(dimension=7), request_device_diagnostic, request_zones_with_failures, request_zone_diagnostic, request_fault_counts; no *22## request; bool / non-int guards on every validator.
  • F4 / F5: WHO 16 steps 5001..5015 / 5101..5115 (builders and decode; 4001 / 4101 asserted absent from the golden file), store_station 1..5 with the WHO 22 store_preset note, dimension 10 decode removed; *16*5001*101##, *16*5101*101##, *#16*101*#10*5## pinned positively against tests/golden/firmware_oracle.json.
  • F6: WHO 22 step_frequency_up/down -> *22*5#S*W## / *22*6#S*W## (1..15) with frequency_step and the step flags; WHAT 7 / 8 gone; *22*5#1*2#1## / *22*6#1*2#1## join the conformance parametrisation with the seeks, plus a positive pin on all eight ack gateways.
  • F7: the F500-trace attribution of the *0## frame removed; all kb_who_* slugs replaced by the ownkb:* ids you listed.

2786 passed, verify_coverage.py 6,986 / 6,986, ruff, mypy, library standards and sync_documentation.py --check clean. Judgement calls are in the body (step flags only for 1..15; request_central_diagnostic defaults to dimension 7; the 4 / 1004 catalogue row text rewritten to the corrected scopes).

@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as ready for review November 2, 2026 19:10

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @gdluck — exemplary rework on 3b76ab1f. Every finding (F1–F7) and all four judgement calls match libdiag.so / libwhere.so / bt_difson / bt_multi, tests/golden/firmware_oracle.json, and the Machine KB (ownkb:*), with comprehensive fail-closed and non-integer/bool guards, synced WHO catalog docs, and all 8 CI gates (including the MyHOME suite) green. Approved and merging!

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit fb0acd7 into OpenWebNet-HA:master Nov 2, 2026
8 checks passed
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck Since #153 is now merged (fb0acd7d) before 2.0.0b12 was published to PyPI, could you open a quick chore/release-2.0.0b13 PR (chore(release): bump version to 2.0.0b13) on top of master (fb0acd7d) so I can approve and cut v2.0.0-b13 directly to PyPI?

  1. OWNd/__init__.py: __version__ = "2.0.0b13"
  2. CHANGELOG.md: promote the current [Unreleased] section to ## [2.0.0b13] - 2026-11-02 (keeping a fresh empty ## [Unreleased] above it and leaving ## [2.0.0b12] - 2026-11-01 intact below it).

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.

3 participants