Repository navigation
feat(load): add WHO 3 load control parser and builders from firmware evidence - #104
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
1ed9f60 to
4e4a976
Compare
…properties feat(load): add typed measurement properties for WHO 3 dimensions 1-4 from firmware register maps (#104)
…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>
…based on firmware oracle
984584d to
3582df6
Compare
fedem95
left a comment
There was a problem hiding this comment.
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_energiagateways agree; - in that fixture
*#3*#N##and*#3*10*D##reply NACK on everybt_energiagateway 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
|
Thank you @fedem95 — your review was 100% spot-on across every point. Root Cause & Primary Source Audit
Fixes Applied
|
fedem95
left a comment
There was a problem hiding this comment.
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.
…t-HA#48 alignment and NACK-with-forwarding
WHO 3 Load Control: dedicated parser and builders from firmware evidence
Adds
OWNLoadEventandOWNLoadCommand(OWNd/message/load.py). Before this PR, WHO 3 frames went through the genericOWNEvent/OWNCommandpass-through. This supersedes the WHO 3 part of #85.1. Primary Sources
WHO 3Specification v1.0.0 (2006-08-30):WHO_3.pdf(OpenWebNet_Community_6_powermanagement_v1_0_0_EN.doc, SHA-2564f685a6ee...) — Section 1.3 DIMENSION table (p. 4) and Sections 1.4.4–1.4.8 (pp. 6–7):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 — unit not specified in the document)functional/who-3-load-management/README.md#measurement-dimension-valuesbt_energiav2.0.2 (2003-05-08) ARM Binary:oracle/cases/energy-ts10.casesandfindings/MyHomeServer1/energy-ts10.md(corrected inown-firmware-oracle#48andown-firmware-oracle#50), vendored and hash-pinned intests/golden/firmware_oracle.json.bt_energiaBinary Disassembly (0x00b140outbound register polling &0x00a63cinbound register decoding):0x00b140(jump table at0x00b3e0) translates*#3*10*D##into SCS register read requests ($03AA0016xx).0x00a63c..0x00b024matches F421 response pairs$031900<REG>(0x00a87c..0x00a8b0) +$031A00<HH>(0x00a8b4..0x00aa20), converts the hex nibbles viahtod, and formats every dimension with%d(unsigned integer format strings, never floats):DIMENSION 1(LOAD_DIMENSION_VOLTAGE,Tin V): polls 1 register ($03AA001613at0x00b674), decodes 1 byte (8-bit int 0..255at0x00af2c), formatssprintf(buf, "*#3*10*1*%d##", val).DIMENSION 2(LOAD_DIMENSION_CURRENT,Cin A): polls 1 register ($03AA001612at0x00b6f4), decodes 1 byte (8-bit int 0..255at0x00ae7c), formatssprintf(buf, "*#3*10*2*%d##", val).DIMENSION 3(LOAD_DIMENSION_POWER,Pin W): polls 2 registers ($03AA001610,$03AA001611at0x00b774), decodes 2 bytes (16-bit int 0..65535at0x00ad88), formatssprintf(buf, "*#3*10*3*%d##", val).DIMENSION 4(LOAD_DIMENSION_ENERGY,E): polls 4 registers ($03AA001606..$03AA001609at0x00b830), decodes 4 bytes (32-bit intat0x00ac34), formatssprintf(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, 09at0x00b3f4) and formatssprintf(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_energiagateways (F453AV030014,F454020051,F459020105,H4684020054,MH200020102,MH200N010108, andMH202010024) agree across all 15 WHO 3 inputs in the oracle fixture with no row lag:bt_energiaGateways (F453AV/F454/F459/H4684/MH200/MH200N/MH202)*3*2*#N##(force)$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)$03AA001614(0x00b374) to the bus (opposite to the spec's "ACK if command is sent to Bus", p. 6, becausebt_energiawaits up to 4 s for an F421 reply before ACKing)OWNLoadCommand.status(priority)*#3*10*D##, D = 0–4 (measurements)$03AA0016xxregister 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 WHERE0*#3*0##(general status)bt_energiaat0x00b91c/0x00b998checks*#3##and*#3*##)Events (
OWNLoadEvent):*3*WHAT*#N##are exposed asstate(LOAD_STATE_DISABLED,_ENABLED,_FORCED,_FORCING_REMOVED),is_forcedandpriority. The#stays part of the WHERE, as the specification requires;priorityis the 1–8 integer.WHERE=10):*#3*10*D*VAL…##values are exposed both as raw strings inmeasurement_valuesand as typed integer properties matchingWHO_3.pdfv1.0.0 andbt_energia%dformatting: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 = 0populates(voltage, current, power, energy)inT*C*P*Eorder.OWNLoadEventand keeps its raw text, so nothing is dropped.Gateway capability & profile gating:
bt_energia(F454,F459,MH202,H4684,F453AV,MH200,MH200N) handle WHO 3 frames and emit the exact 9 SCS bus telegrams.bt_energia(MyHomeServer1,F461,F450,F460) immediately returnnack (silent)with0bus frames for all 15 WHO 3 inputs.OWNd/profiles.py,MyHomeServer1ProfileandF461Profileexplicitly excludeWHO_LOAD_CONTROLfrom theirsupported_whotuples, whileMH200ProfileandMH200NProfileincludeWHO_LOAD_CONTROL.3. How is it tested?
tests/test_who3_load_control.py+tests/test_profiles.py, driven by the vendored oracle:OWNLoadEvent/OWNLoadCommand.force(1)andforce(2)are ACKed by all 7bt_energiagateways and produce$04B70N1300. The event-session echoes*3*2*#1##and*3*2*#2##parse as forced, with the right priority.*#3*#N##/*#3*10*D##): verified across all 7bt_energiagateways to emit the expected$03AA0016xxtelegrams while replying NACK on the command session in the oracle fixture.bt_energiagateways.test_gateways_without_bt_energia_refuse_all_who3_frames: verifies that gateways lackingbt_energia(MyHomeServer1,F461,F450,F460) refuse all 15 WHO 3 frames with 0 bus output.1: 0x13,2: 0x12,3: 0x10..0x11,4: 0x06..0x09,0: all 8) andvoltage/current/power/energyproperties are verified across all 7bt_energiagateways.test_myhomeserver1_lacks_who3_load_controlensuresMyHomeServer1ProfileandF461Profileexclude WHO 3 whileF454Profile,MH202Profile,MH200Profile, andMH200NProfileinclude WHO 3.OWNd/message/load.pyhas 100.0% statement and branch coverage;ruffandscripts/sync_documentation.py --checkpass cleanly.