Skip to content

feat(load): add WHO 3 load control parser and builders from firmware evidence - #104

Merged
GreenGrassBlueOcean merged 5 commits into
masterfrom
feat/who3-load-control
Oct 16, 2026
Merged

GreenGrassBlueOcean merged 5 commits into
masterfrom
feat/who3-load-control

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

WHO 3 Load Control: dedicated parser and builders from firmware evidence

Adds OWNLoadEvent and OWNLoadCommand (OWNd/message/load.py). Before this PR, WHO 3 frames went through the generic OWNEvent / OWNCommand pass-through. This supersedes the WHO 3 part of #85.


1. Primary Sources

  1. Official WHO 3 Specification v1.0.0 (2006-08-30):
  2. Firmware Oracle & bt_energia v2.0.2 (2003-05-08) ARM Binary:
    • Oracle Suite: oracle/cases/energy-ts10.cases and findings/MyHomeServer1/energy-ts10.md (corrected in own-firmware-oracle#48 and own-firmware-oracle#50), vendored and hash-pinned in tests/golden/firmware_oracle.json.
    • bt_energia Binary Disassembly (0x00b140 outbound register polling & 0x00a63c inbound register decoding):
      • 0x00b140 (jump table at 0x00b3e0) translates *#3*10*D## into SCS register read requests ($03AA0016xx).
      • 0x00a63c..0x00b024 matches F421 response pairs $031900<REG> (0x00a87c..0x00a8b0) + $031A00<HH> (0x00a8b4..0x00aa20), converts the hex nibbles via htod, and formats every dimension with %d (unsigned integer format strings, never floats):
        • DIMENSION 1 (LOAD_DIMENSION_VOLTAGE, T in V): polls 1 register ($03AA001613 at 0x00b674), decodes 1 byte (8-bit int 0..255 at 0x00af2c), formats sprintf(buf, "*#3*10*1*%d##", val).
        • DIMENSION 2 (LOAD_DIMENSION_CURRENT, C in A): polls 1 register ($03AA001612 at 0x00b6f4), decodes 1 byte (8-bit int 0..255 at 0x00ae7c), formats sprintf(buf, "*#3*10*2*%d##", val).
        • DIMENSION 3 (LOAD_DIMENSION_POWER, P in W): polls 2 registers ($03AA001610, $03AA001611 at 0x00b774), decodes 2 bytes (16-bit int 0..65535 at 0x00ad88), formats sprintf(buf, "*#3*10*3*%d##", val).
        • DIMENSION 4 (LOAD_DIMENSION_ENERGY, E): polls 4 registers ($03AA001606..$03AA001609 at 0x00b830), decodes 4 bytes (32-bit int at 0x00ac34), formats sprintf(buf, "*#3*10*4*%d##", val).
        • DIMENSION 0 (LOAD_DIMENSION_ALL, T*C*P*E): polls all 8 registers (13, 12, 10, 11, 06, 07, 08, 09 at 0x00b3f4) and formats sprintf(buf, "*#3*10*0*%s*%s*%s*%s##", T, C, P, E) (0x00aa24).

2. How is it solved?

Commands: only what the firmware puts on the bus. All seven bt_energia gateways (F453AV 030014, F454 020051, F459 020105, H4684 020054, MH200 020102, MH200N 010108, and MH202 010024) agree across all 15 WHO 3 inputs in the oracle fixture with no row lag:

Frame bt_energia Gateways (F453AV / F454 / F459 / H4684 / MH200 / MH200N / MH202) Builder
*3*2*#N## (force) ACK, bus $04B70N1300 (priority in the telegram); echoed on the event session by F453AV, F459, H4684, MH200, MH200N and MH202 (all except F454) OWNLoadCommand.force(priority)
*#3*#N## (priority status) NACK on the command session while still forwarding $03AA001614 (0x00b374) to the bus (opposite to the spec's "ACK if command is sent to Bus", p. 6, because bt_energia waits up to 4 s for an F421 reply before ACKing) OWNLoadCommand.status(priority)
*#3*10*D##, D = 0–4 (measurements) NACK on the command session while still forwarding the 1/1/2/4/8 $03AA0016xx register reads (13, 12, 10..11, 06..09) to the bus (opposite to the spec's "ACK if command is sent to Bus", p. 6) OWNLoadCommand.request_measurement(dimension)
*3*0*…, *3*1*…, *3*3*…, general WHERE 0 NACK, nothing on the bus none
*#3*0## (general status) NACK, nothing on the bus (bt_energia at 0x00b91c/0x00b998 checks *#3## and *#3*##) none

Events (OWNLoadEvent):

  • Priority states: WHAT 0–3 on *3*WHAT*#N## are exposed as state (LOAD_STATE_DISABLED, _ENABLED, _FORCED, _FORCING_REMOVED), is_forced and priority. The # stays part of the WHERE, as the specification requires; priority is the 1–8 integer.
  • Measurements (WHERE=10): *#3*10*D*VAL…## values are exposed both as raw strings in measurement_values and as typed integer properties matching WHO_3.pdf v1.0.0 and bt_energia %d formatting:
    • voltage: int | None (LOAD_DIMENSION_VOLTAGE = 1, Volt)
    • current: int | None (LOAD_DIMENSION_CURRENT = 2, Ampere)
    • power: int | None (LOAD_DIMENSION_POWER = 3, Watt)
    • energy: int | None (LOAD_DIMENSION_ENERGY = 4, raw 32-bit integer energy counter)
    • LOAD_DIMENSION_ALL = 0 populates (voltage, current, power, energy) in T*C*P*E order.
  • Unknown frames: a WHO 3 frame with an unknown WHAT or dimension still parses into OWNLoadEvent and keeps its raw text, so nothing is dropped.

Gateway capability & profile gating:

  • Gateways equipped with bt_energia (F454, F459, MH202, H4684, F453AV, MH200, MH200N) handle WHO 3 frames and emit the exact 9 SCS bus telegrams.
  • Gateways lacking bt_energia (MyHomeServer1, F461, F450, F460) immediately return nack (silent) with 0 bus frames for all 15 WHO 3 inputs.
  • Accordingly, in OWNd/profiles.py, MyHomeServer1Profile and F461Profile explicitly exclude WHO_LOAD_CONTROL from their supported_who tuples, while MH200Profile and MH200NProfile include WHO_LOAD_CONTROL.

3. How is it tested?

tests/test_who3_load_control.py + tests/test_profiles.py, driven by the vendored oracle:

  • Dispatch: all 15 WHO 3 oracle inputs dispatch to OWNLoadEvent / OWNLoadCommand.
  • Force: force(1) and force(2) are ACKed by all 7 bt_energia gateways and produce $04B70N1300. The event-session echoes *3*2*#1## and *3*2*#2## parse as forced, with the right priority.
  • Status & Measurement Requests (*#3*#N## / *#3*10*D##): verified across all 7 bt_energia gateways to emit the expected $03AA0016xx telegrams while replying NACK on the command session in the oracle fixture.
  • Builders vs firmware:
    • Every builder frame that the oracle contains is forwarded by all 7 bt_energia gateways.
    • No builder, across all valid arguments, can produce a frame that the gateways refuse.
    • test_gateways_without_bt_energia_refuse_all_who3_frames: verifies that gateways lacking bt_energia (MyHomeServer1, F461, F450, F460) refuse all 15 WHO 3 frames with 0 bus output.
  • Registers & Measurement Properties: the per-dimension register runs (1: 0x13, 2: 0x12, 3: 0x10..0x11, 4: 0x06..0x09, 0: all 8) and voltage/current/power/energy properties are verified across all 7 bt_energia gateways.
  • Profile gating: test_myhomeserver1_lacks_who3_load_control ensures MyHomeServer1Profile and F461Profile exclude WHO 3 while F454Profile, MH202Profile, MH200Profile, and MH200NProfile include WHO 3.
  • Coverage & Linters: OWNd/message/load.py has 100.0% statement and branch coverage; ruff and scripts/sync_documentation.py --check pass cleanly.

@codecov-commenter

codecov-commenter commented Oct 9, 2026 •

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 added a commit that referenced this pull request Oct 16, 2026
…properties

feat(load): add typed measurement properties for WHO 3 dimensions 1-4 from firmware register maps (#104)
GreenGrassBlueOcean and others added 4 commits October 16, 2026 20:48
…evidence

OWNLoadEvent / OWNLoadCommand replace the base-class pass-through for WHO 3.

Builders exist only for frames the firmware oracle (energy-ts10, F459 020105
and MH202 010024) shows reaching the bus: force *3*2*#N## (bus $04B70N1300),
priority status *#3*#N## and control-unit measurements *#3*10*D##. WHAT 0/1/3,
general WHERE 0 and *#3*0## are refused by the gateways and get no builder.

State replies (WHAT 0-3) follow the WHO 3 v1.0.0 specification. Measurement
replies stay raw: no device capture confirms their scaling or units yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@fedem95 fedem95 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.

Tested 3582df6: 1539 passed on the branch. Merged with current master, the only failures are the 9 test_who18_stop_and_go oracle tests, which fail on master alone too. The command side and the profile gating look right to me. One blocker on the measurement side, plus description drift.

Measurement names and order. 5f43444 maps dimension 1 to power (W), 2 to voltage (V), 3 to current (A), 4 to power factor, and dimension 0 as power, voltage, current, power factor. The spec summary that load.py's own docstring cites (Encyclopedia functional/who-3-load-management) says 1 voltage (V), 2 current (A), 3 power (W), 4 energy (unit not specified), and 0 as voltage, current, power, energy. The code comment cites findings/MyHomeServer1/energy-ts10.md, which names them but gives no source; the bus telegrams carry only register numbers ($03AA0016xx), and energy-ts10.cases names no quantity. (My guess only: four registers for dimension 4 fits an energy counter better than a power factor.) Until a capture says otherwise, I'd keep the values raw as the description says, or follow the spec order without units.

Description vs code:

  • it says values stay raw with no units and the log only says "(voltage per specification)", but the code logs e.g. "Load control unit power draw: X W.", and README / the WHO catalogue mention typed power, voltage, current and power factor properties;
  • the one-row lag of F453AV, F454, H4684 and MH200N is gone in the fixture now on master (oracle #48): all bt_energia gateways agree;
  • in that fixture *#3*#N## and *#3*10*D## reply NACK on every bt_energia gateway while still putting the telegrams on the bus (8 for dimension 0), and *3*2*#N## is ACKed. Worth saying, since a client will see NACK there.

…, 4=energy) with WHO_3.pdf and bt_energia binary
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

Thank you @fedem95 — your review was 100% spot-on across every point.

Root Cause & Primary Source Audit

  1. Why the wrong mapping appeared in 5f43444:

    • Commit 5f43444 copied the parenthetical dimension names (1: instantaneous power, 2: voltage, 3: current, 4: power factor) from own-firmware-oracle/findings/MyHomeServer1/energy-ts10.md.
    • Those labels in energy-ts10.md were unverified prose: libqtdevices (TS10_1_0_23) devices/loads_device.cpp is a WHO 18 consumer (as documented in myopencommunity-energy-history-review.md), and the QEMU oracle only observed the outbound $03AA0016xx bus requests because no F421 central unit was emulated on the virtual SCS bus.
  2. Official WHO_3.pdf v1.0.0 (2006-08-30) (pp. 4, 6–7) & OpenWebNet Encyclopedia:

    • DIMENSION 0: All dimensions (*#3*10*0*T*C*P*E##)
    • DIMENSION 1: Voltage (T, Volt)
    • DIMENSION 2: Current (C, Ampere)
    • DIMENSION 3: Power (P, Watt)
    • DIMENSION 4: Energy (E, Measured Energy)
  3. bt_energia v2.0.2 (2003-05-08) ARM Binary Disassembly:

    • Disassembling bt_energia (0x00b140 outbound register polling via jump table at 0x00b3e0, and 0x00a63c..0x00b024 inbound $031900<REG> + $031A00<HH> decoding via htod) confirms your register-width observation:
      • Dim 1 (LOAD_DIMENSION_VOLTAGE = 1): polls 1 register ($03AA001613 at 0x00b674), decodes 1 byte (8-bit int 0..255 at 0x00af2c), formats sprintf(buf, "*#3*10*1*%d##", val).
      • Dim 2 (LOAD_DIMENSION_CURRENT = 2): polls 1 register ($03AA001612 at 0x00b6f4), decodes 1 byte (8-bit int 0..255 at 0x00ae7c), formats sprintf(buf, "*#3*10*2*%d##", val).
      • Dim 3 (LOAD_DIMENSION_POWER = 3): polls 2 registers ($03AA001610..$03AA001611 at 0x00b774), decodes 2 bytes (16-bit int 0..65535 at 0x00ad88), formats sprintf(buf, "*#3*10*3*%d##", val).
      • Dim 4 (LOAD_DIMENSION_ENERGY = 4): polls 4 registers ($03AA001606..$03AA001609 at 0x00b830), decodes 4 bytes (32-bit int at 0x00ac34), formats sprintf(buf, "*#3*10*4*%d##", val).
      • Dim 0 (LOAD_DIMENSION_ALL = 0): polls all 8 registers (13, 12, 10, 11, 06..09 at 0x00b3f4) and joins them with sprintf(buf, "*#3*10*0*%s*%s*%s*%s##", T, C, P, E) (0x00aa24).

Fixes Applied

@fedem95 fedem95 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.

Re-tested 739d551: 1539 passed on the branch; merged with master, the only failures are the 9 test_who18_stop_and_go tests that fail on master alone (master bff6884 is red on Tests & Coverage), so the red checks here aren't from this PR. Dimension order now matches WHO_3.pdf: I checked the archived copy (SHA-256 matches the link), where p. 4 lists 1 Voltage, 2 Current, 3 Power, 4 Energy and p. 6 gives Volt / Ampere / Watt with no unit for energy. I can't check the bt_energia addresses myself, but the register counts (1, 1, 2, 4) match the oracle. Approving.

Two leftovers from my earlier review, non-blocking:

  • load.py (line 18) and the description still mention a one-row lag for F453AV, F454, H4684 and MH200N; with the oracle #48 fixture now on master there is none;
  • load.py (line 15) and the description say *#3*#N## / *#3*10*D## are forwarded, but in that fixture they get a NACK while the telegrams still go out, which is the opposite of the spec's "ACK if command is sent to Bus" (p. 6). Worth a line, since a client will see the NACK.

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit d922b4a into master Oct 16, 2026
12 of 16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the feat/who3-load-control branch October 16, 2026 21:31
GreenGrassBlueOcean added a commit that referenced this pull request Oct 16, 2026
docs(load): address #104 review nits on oracle #48 alignment and NACK-with-forwarding
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Oct 16, 2026
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