Skip to content

fix(energy): parse the WHO 18 dimension 1200 automatic-update reply - #92

Merged
GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/who18-auto-update-reply
Oct 9, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/who18-auto-update-reply

Conversation

@gdluck

@gdluck gdluck commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by A · project thread

Before: *#18*51*1200#1*255## produced an OWNEnergyEvent with no message type and no value.

After: message_type = auto_update_interval, update_interval = 255 (minutes duration, 0 = stream stopped), energy_type = 1 (1 electricity, 2 gas, 3 heat, 4 water).

Evidence

Legrand WHO 18 Specification. Legrand/BTicino "Open Web Net Language, WHO = 18 Energy Management" v1.0.0 (2011-07-15):

  • Start/request form: *#18*<Where>*#1200#<Type>*<Time>## where Time is stream duration in minutes (1..255).
  • Event/reply form: *#18*<Where>*1200#<Type>*<Time>##.
  • Stop form: *#18*<Where>*#1200#<Type>*0## (Time = 0 indicates streaming stopped).
  • Note on types: The official specification defines Type 1 = active power; energy types 2 (gas), 3 (heat), and 4 (water) are sourced from BTicino's touch screen client libqtdevices.

libqtdevices TS10_1_0_23. energy_device.cpp:

 50: enum EnergyType
 52: 	TYPE_ELECTRICITY = 1,
 53: 	TYPE_GAS = 2,
 54: 	TYPE_HEAT = 3,
 55: 	TYPE_WATER = 4,
111: 	_DIM_STATE_UPDATE_INTERVAL             = 1200,   // used to detect start/stop of automatic updates
251: 	emit sendFrame(createDimensionFrame("18", QString("#%1#%2*%3").arg(_DIM_STATE_UPDATE_INTERVAL)
252: 					    .arg(modeToEnergyType(mode)).arg(UPDATE_INTERVAL), where));
289: void AutomaticUpdates::handleAutomaticUpdate(OpenMsg &msg)
291: 	int time = msg.whatArgN(0);
301: 	if (time == 0)
303: 		qDebug("Received auto-update stop frame");
307: 		case UPDATE_AUTO:
308: 			// restart automatic updates since we need them
309: 			sendUpdateStart();
719: 	else if (what == _DIM_STATE_UPDATE_INTERVAL && msg.whatArgCnt() == 1)
721: 		current_updates->handleAutomaticUpdate(msg);

The client sends *#18*W*#1200#type*time## to start the updates and parses the 1200#type*time reply, re-arming the stream when time is 0.

Firmware. bt_supervisione, identical on MyHomeServer1, F454, F459 and F461 (results/spv*.tsv):

Frame Reply Bus body
*#18*51*#1200#1*255## ACK D1 A1 02 32 00 02 1D FF
*#18*51*#1200#1*0## ACK D1 A1 02 32 00 02 1D 00
*#18*51*#1200#2*255## ACK D1 A1 02 32 00 02 82 FF
*#18*51*1200## NACK none
*#18*51*1200#1## NACK none

The write form carries the type (1D electricity, 82 gas) and the duration (FF / 00) on the bus; the bare request forms never leave the gateway, which is why OWNd should expect the reply only as an answer to the write (and why MyHOME's *#18*W*1200## poll is dead, resolved in MyHOME companion PR #661).

On-wire Customer Bus Capture. Grounded in physical F454 trace capture contributed by @xtimmy86x in OpenWebNet-HA/MyHOME#664 (tests/fixtures/traces/issue_466/myhome_trace_F454_who18_2026-10-08T19-30-16.json). In the live trace, *#18*56*#1200#1*2## is sent at 19:27:55.541 and *#18*56*1200#1*0## arrives at 19:30:01.726 (~126 s later), empirically confirming the duration-in-minutes lifecycle.

How

OWNd/message/energy.py:

  • Added elif self._dimension == 1200 branch in OWNEnergyEvent parsing update_interval (minutes duration) and energy_type.
  • Exported MESSAGE_TYPE_AUTO_UPDATE_INTERVAL in OWNd/message/__init__.py::__all__.
  • Added symmetrical builder OWNEnergyCommand.stop_sending_instant_power(where, energy_type=1) (*#18*W*#1200#type*0##).
  • Parameterized energy_type in OWNEnergyCommand.start_sending_instant_power(where, duration=65, energy_type=1).
  • Tests: tests/test_who18_auto_update_reply.py. pytest: 1240 passed, 1 skipped.

How to check. libqtdevices quotes are from tag TS10_1_0_23 of OpenWebNet-HA/libqtdevices (BTicino's 2010 touch-screen client, used only as a second client, not as the reference). Firmware rows are replays of the exact frame on the original gateway translator binaries (ARM, from the MyHomeServer1, F454, F459, F461 and F450 images) under QEMU on a fake SCS bus: ACK = *#*1##, NACK = *#*0##, the bus body is what the program put on the SCS bus (hex, without the A8 / checksum / A3 framing). Spec = Legrand/BTicino "Open Web Net Language, WHO = 18 Energy Management" v1.0.0 (2011-07-15). Raw logs, command lists, the harness and the report are in firmware_validation.zip (results/<program>[_<image>].tsv, results/*.cl / *.stub / *.mon, REPORT.md). The firmware images themselves are not included.

Split out of #86 so each change can be checked on its own; the eight slices (#87-#94) merge together in any order without conflicts, and the merged union passes 1285 tests with 100 % coverage of OWNd/message (checked locally on 2026-10-07 after the review rework of #94).

🤖 Generated with Claude Code

https://claude.ai/code/session_018oHp1P4vxxoFnaLUUZpbpZ

*OpenWebNet-HA#18*W*1200#type*time## is the meter's answer to the automatic power
update request *OpenWebNet-HA#18*W*#1200#type*time##: time > 0 confirms the stream,
time = 0 says it stopped. OWNd left it untyped. libqtdevices TS10_1_0_23
names the dimension (_DIM_STATE_UPDATE_INTERVAL = 1200,
energy_device.cpp:111), re-arms the stream when time is 0
(:289-322, :719-726) and defines type 1 electricity, 2 gas, 3 heat,
4 water (:50-56). The gateway firmware (bt_supervisione on MyHomeServer1,
F454, F459, F461) accepts the write forms #1200#1*255, #1200#1*0 and
#1200#2*255 with distinct bus frames, and refuses the bare request
*OpenWebNet-HA#18*W*1200## with no bus frame.

OWNEnergyEvent gains message type auto_update_interval with
update_interval and energy_type.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oHp1P4vxxoFnaLUUZpbpZ
@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 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.

Review: WHO 18 Dimension 1200 Auto-Update Reply (OWNEnergyEvent)

Verdict: Approve with Structural Prerequisites & Code Fixes (Protocol-accurate and eliminates silent unhandled frames, but requires __all__ export fix, trace fixture backing, and a companion integration PR in MyHOME).

Thanks @gdluck! This PR correctly addresses the incoming confirmation and stop replies for automatic energy streaming (*#18*W*1200#type*time##).

Before merging into OWNd, we have two structural prerequisites and two code fixes:


1. Authentic Bus Trace in MyHOME (PR Required)

We audited the trace corpus in MyHOME/tests/fixtures/traces/ across all gateway captures (F454, MH200, MH201, MyHomeServer1):

  • Existing traces: In customer logs (issue_629), we have authentic traces of MyHOMEPowerSensor sending invalid bare read requests *#18*WHERE*1200## which gateway firmware drops with NACK (Could not send message *#18*74#0*1200##. No more retries.). In issue_649, we have empirical confirmation that *#18*51*#1200#1*255## ACKs while *#18*51*1200## NACKs.
  • Prerequisite: We currently lack an authentic customer on-wire bus capture in tests/fixtures/traces/ showing a meter emitting the incoming reply frames *#18*51*1200#1*255## and *#18*51*1200#1*0##. To protect against regressions and ground this in empirical data, we need a trace fixture PR in MyHOME with these frames.

2. Companion PR in MyHOME (Downstream Consumer)

OWNEnergyEvent serves downstream consumers—specifically MyHOMEPowerSensor in custom_components/myhome/sensor.py:

  1. Eliminate Dead Polling Frame: MyHOMEPowerSensor.async_update currently sends *#18*{where}*1200## for periodic polling. Gateway firmware (bt_supervisione) always NACKs this request because Dimension 1200 is strictly a write/event dimension. A companion PR in MyHOME is needed to eliminate this dead polling frame.
  2. Handle Auto-Update Telemetry: MyHOMEPowerSensor.handle_event currently ignores non-power events. The companion PR will intercept MESSAGE_TYPE_AUTO_UPDATE_INTERVAL to track active streaming timers and detect when the meter halts streaming (update_interval == 0).

3. Firmware Oracle & Binary Evidence

  • Firmware Verification (bt_supervisione):
    ARM binary reverse engineering and QEMU execution across MyHomeServer1 (028206), F454 (020051), F459, and F461 confirm exact SCS bus telegram dispatch:
    • *#18*51*#1200#1*255## $\rightarrow$ ACK $\rightarrow$ SCS D1 A1 02 32 00 02 1D FF (Type 1D = active power, Interval FF = 255)
    • *#18*51*#1200#1*0## $\rightarrow$ ACK $\rightarrow$ SCS D1 A1 02 32 00 02 1D 00 (Stream stop)
    • *#18*51*#1200#2*255## $\rightarrow$ ACK $\rightarrow$ SCS D1 A1 02 32 00 02 82 FF (Type 82 = gas, Interval FF)
    • *#18*51*1200## $\rightarrow$ NACK (*#*0##, dropped at gateway router boundary; never reaches SCS bus)
  • Encyclopedia Machine KB:
    functional/who-18-energy-management/dimensions.md (ownkb:chunk:r000421):
    • Start/update form: *#18*WHERE*#1200#Type*Time## (Type 1 = active power, Time = 1..255)
    • Stop form: *#18*WHERE*#1200#Type*0## (Time = 0)
    • Reply form: *#18*WHERE*1200#Type*Time##

4. Code Fixes in This PR (OWNd)

A. Missing from __all__ in OWNd/message/__init__.py (Must Fix)

MESSAGE_TYPE_AUTO_UPDATE_INTERVAL is imported on line 43 of OWNd/message/__init__.py, but is omitted from __all__ (lines 198–207). Please add it to __all__:

    # Energy (WHO 18)
    "MESSAGE_TYPE_ACTIVE_POWER",
    "MESSAGE_TYPE_ENERGY_TOTALIZER",
    "MESSAGE_TYPE_HOURLY_CONSUMPTION",
    "MESSAGE_TYPE_DAILY_CONSUMPTION",
    "MESSAGE_TYPE_MONTHLY_CONSUMPTION",
    "MESSAGE_TYPE_CURRENT_DAY_CONSUMPTION",
    "MESSAGE_TYPE_CURRENT_MONTH_CONSUMPTION",
    "MESSAGE_TYPE_AUTO_UPDATE_INTERVAL",  # <-- Add
    "_integer_value",
    "OWNEnergyEvent",
    "OWNEnergyCommand",

B. Symmetrical Command Builder in OWNEnergyCommand

OWNEnergyCommand implements start_sending_instant_power, but lacks a companion method to stop streaming (duration = 0) or select energy types 2–4:

    @classmethod
    def stop_sending_instant_power(
        cls, where: str | int, energy_type: int = 1
    ) -> OWNEnergyCommand:
        """Stop automatic power updates (interval 0)."""
        where = f"{where}#0" if str(where).startswith("7") else str(where)
        message = cls(f"*#18*{where}*#1200#{energy_type}*0##")
        message._human_readable_log = f"Stopping instant power updates from sensor {where}."
        return message

C. Log Message Duration Units

In OWNEnergyCommand.start_sending_instant_power (line 218), the log states "for {duration} minutes", whereas in OWNEnergyEvent.__init__ (line 173), it states "every {self._update_interval} s". Per Legrand's SCS specification WHO_18.pdf, Time is minutes (the ZigBee variant defines seconds). Clarifying the units in the event log or docstring prevents user confusion.

@GreenGrassBlueOcean

GreenGrassBlueOcean commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Both structural prerequisites in MyHOME are now ready and open for review:

  1. Trace Fixture & Replay Test PR: OpenWebNet-HA/MyHOME#660

    • Contributes an end-to-end replay fixture and test suite for the WHO 18 Dimension 1200 energy streaming lifecycle (myhome_trace_energy_dimension1200_stream.json).
    • Telemetry frames (*#18*51*113*377## .. 500##) and gateway profile (MyHomeServer1 2.87.13) are extracted verbatim from authentic physical plant capture myhome_trace_MyHomeServer1_all_2026-10-01T07-59-11.json (#466).
    • Dimension 1200 control handshake sequence (*#18*51*#1200#1*125## start, *#18*51*1200#1*125## confirmation, *#18*51*#1200#1*0## stop) is modeled from Legrand WHO_18.pdf and gateway firmware execution (bt_supervisione).
    • Adds golden frame fixtures in tests/golden/frames/who18_energy.yaml with roundtrip and builder parity tests.
  2. Downstream Integration Companion PR: OpenWebNet-HA/MyHOME#661

    • Replaces the dead, NACKed polling query *#18*{where}*1200## in MyHOMEPowerSensor.async_update with canonical Dimension 113 active power status requests (*#18*{where}*113##).
    • Consumes MESSAGE_TYPE_AUTO_UPDATE_INTERVAL (and fallback to raw Dimension 1200) in MyHOMEPowerSensor.handle_event to track stream lease windows and detect when the meter terminates streaming (update_interval == 0).
    • Adds async_stop_sending_instant_power to gracefully stop active streams via duration 0 write frames (*#18*{where}*#1200#1*0##).
    • Maintained dual compatibility for seamless operation across both OWNd 2.0.0b10 and 2.0.0b11 with strict 100.0% unit test coverage.

Once the minor code fixes in this PR (exporting MESSAGE_TYPE_AUTO_UPDATE_INTERVAL in OWNd/message/__init__.py::__all__ and symmetrical stop_sending_instant_power builder) are pushed, this is ready to approve and merge!

…e the unit

Review follow-ups on PR 92: MESSAGE_TYPE_AUTO_UPDATE_INTERVAL is listed in
__all__; OWNEnergyCommand.stop_sending_instant_power(where, energy_type)
builds *OpenWebNet-HA#18*W*#1200#type*0## (firmware: ACK, bus D1 A1 02 32 00 02 1D 00)
and start_sending_instant_power takes the energy type too (default 1,
frames unchanged); the event log and docstring say the Time field is in
minutes, as the start builder already did (Legrand WHO 18).

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

gdluck commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. The two code fixes are in 8463824:

  • A: MESSAGE_TYPE_AUTO_UPDATE_INTERVAL is now in __all__ (test checks it).
  • B: OWNEnergyCommand.stop_sending_instant_power(where, energy_type=1) builds *#18*W*#1200#type*0##, and start_sending_instant_power gained the same optional energy_type argument (default 1, existing frames unchanged). Tests cover the stop form, type 2 and the 7xx#0 address.
  • C: the event log and the update_interval docstring now say minutes, matching the start builder and Legrand WHO 18.

I read MyHOME #660 and #661 as the prerequisites; nothing else is needed on this side for them.

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

Blocked: Missing Authentic On-Wire Customer Bus Traces

Per OpenWebNet-HA repository and protocol verification standards, PR #92 cannot be merged in its current state.

1. Mandatory Trace Gate

  • All protocol parsers and event types in OWNd must be grounded in authentic, untouched on-wire customer plant bus traces.
  • A thorough audit of the entire trace corpus across all gateway captures (MyHomeServer1, F454, F459, MH200, MH201, MH202, H4890) confirms that we do not have an authentic customer on-wire bus capture of an energy meter emitting the *#18*WHERE*1200#Type*Time## incoming reply frame on a live plant.
  • A proposed companion trace PR (MyHOME#660) was withdrawn and closed because it contained synthesized Dimension 1200 frames, which violates the foundational rule: "They are facts: never edit or synthesize a frame."

2. What We Know Empirically

  • Firmware Verification (bt_supervisione): We have verified via binary execution that *#18*WHERE*#1200#1*Time## start/stop write frames produce valid SCS telegrams (Opcode 1D) and ACK, while bare reads *#18*WHERE*1200## NACK.
  • Telemetry: Physical plant captures (issue_466) show continuous passive active power frames *#18*WHERE*113*P##.
  • Missing Evidence: The exact on-wire behavior of a physical energy meter (e.g. BTicino F520) emitting incoming *#18*WHERE*1200#Type*Time## replies on the bus has not been captured in an untouched customer log.

3. Conditions to Unblock

This PR remains strictly blocked until:

  1. An authentic, unedited on-wire customer trace capturing physical meter responses for *#18*WHERE*1200#Type*Time## is contributed to MyHOME/tests/fixtures/traces/.
  2. The code fixes noted in the earlier review (adding MESSAGE_TYPE_AUTO_UPDATE_INTERVAL to OWNd/message/__init__.py::__all__ and symmetrical stop_sending_instant_power builder) are applied.

GreenGrassBlueOcean added a commit to OpenWebNet-HA/MyHOME that referenced this pull request Oct 8, 2026
…and replay test (#466)

Adds an authentic customer on-wire bus trace and deterministic replay
test suite for the complete WHO 18 Dimension 1200 automatic instant power
stream lifecycle (*#18*WHERE*#1200#Type*Time## start command, meter interval
confirmation, periodic active power telemetry, and zero-duration interval
notification emitted upon 2-minute stream lease expiry).

Captured and contributed by @xtimmy86x on PR #661 (#issuecomment-6067730589)
from a physical BTicino F454 gateway (firmware 2.0) and BTicino F520 energy
meters on a live plant.

Companion to:
- OWNd PR OpenWebNet-HA/OWNd#92
- Downstream companion PR #661

Co-authored-by: xtimmy86x <76836856+xtimmy86x@users.noreply.github.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.

Mandatory trace gate satisfied by authentic customer on-wire bus capture in OpenWebNet-HA/MyHOME#664. Code review follow-ups (exporting MESSAGE_TYPE_AUTO_UPDATE_INTERVAL in __all__, stop_sending_instant_power builder, minutes duration unit) verified in commit 8463824. Approving.

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

Approving. Checked against Legrand's WHO_18.pdf (v1.0.0, 2011-07-15): start *#18*<Where>*#1200#<Type>*<Time>## with Time in minutes (1–255), event *#18*<Where>*1200#<Type>*<Time>##, stop with Time 0. On xtimmy86x's F454 trace (MyHOME#664; F520 meters per his comment) the parser turns all four 1200 replies into auto_update_interval with the right value and type, and the other 61 received frames (53 of them *113*) parse exactly as on master. The stop builder gives *#18*56*#1200#1*0## (and the 74#0 form). Suite passes locally (1240, test_metadata excluded for my env), CI green.

Notes:

  • Unit in the description. The Before/After line says update_interval = 255 (seconds, …); the code, docstrings and the PDF say minutes. In the trace *#18*56*#1200#1*2## is sent at 19:27:55.541 and *#18*56*1200#1*0## arrives at 19:30:01.726, 126.2 s later.
  • Log wording. It reads "automatic updates every 2 minutes". In the same trace meter 56 sends *113* about every 30 s, already before the request (19:27:40), and the 0 reply comes about 2 minutes after a request with Time = 2. "every N minutes" doesn't match that; maybe "automatic updates for N minutes"?
  • Energy type. The PDF only defines Type 1 = active power; 2/3/4 (gas, heat, water) come from libqtdevices alone.
  • The description cites "WHO = 4 v2.0.0 (2013-11-27)" as the spec; the WHO 18 document is WHO_18.pdf v1.0.0.

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.

4 participants