From 7024ffa71247989b16eb82efd0e89bc8adbea69b Mon Sep 17 00:00:00 2001 From: gdluck Date: Sat, 24 Oct 2026 18:33:50 +0300 Subject: [PATCH 1/2] docs(heating): central_status deprecation text names the real reason The deprecation docstring and warning of OWNHeatingCommand.central_status said gateways refuse `*#4*WHERE*14##`. In the firmware oracle (trace-corpus) six of the twelve emulated gateways forward it to the bus (nack/out: F453, F453AV, F460, F461, MH200N, MyHomeServer1) and six refuse it (nack/silent); either way it goes unanswered without a central unit (live F461 2.0.11 and MyHomeServer1 3.87.13 NACK it after the bus timeout). The warning now points to status('#0'). The frame is unchanged. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + OWNd/message/heating.py | 25 +++++++++++------ tests/test_who4_central_status_text.py | 39 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 tests/test_who4_central_status_text.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 655b70d..1ba49ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to `OWNd` are documented in this file. ### Fixed - **WHO 4 Dimension 22 is never sent**: Dimension 22 is the published split-unit IR control (`*#4*3#Z#N*22##` / `*#4*3#Z#N*#22*MOD*SP*VEL*SWING##`, WHO 4 specification pp. 66-68); in any spelling (`*#4*ZONE*22##`, `*#4*#0*22##`, leading zeros, whitespace or `+` tokens) it crashes the thermoregulation daemon (`bt_termo`) on Linux IP gateways and reboots an F461 2.0.11 about three minutes later (own-firmware-oracle `tools/expected_divergences.yaml` crash frames, live F461 2.0.11). `OWNCommandSession.send` (and so `AsyncTcpTransport.send` and `keepalive`), `OWNCommandSession.send_to_gateway` (before `connect()`: no socket, no handshake) and `AsyncSerialTransport.send` refuse it before anything is written, with the contract above and `reason="destructive_frame"`. The detector `is_who4_dimension_22()` is public, accepts `bytes`, and is anchored to the first `*` of each frame, so a mid-frame WHERE `#4` / DIMENSION `#4` such as `*#2*#4*#11#001*22##` or `*#1*14*#4*1*22##` stays sendable. Parsing is unchanged: the frame can still be read from the bus. - **`AsyncSerialTransport.send` validates outbound frames** like `OWNCommandSession.send`: an invalid or signaling frame (for example the single-`#` concatenation `*1*1*11#*#4*2*22##`) is refused with `reason="invalid_frame"` instead of being written to the port. The frame is serialised once, terminator included: the string checked is the string written. +- **WHO 4 (`OWNHeatingCommand.central_status`)**: the deprecation text said gateways refuse `*#4*WHERE*14##`; six of the twelve emulated gateways forward it to the bus (own-firmware-oracle `trace-corpus`) and it is simply unanswered without a central unit. The warning now points to `status('#0')`. The frame is unchanged. ## [2.0.0b11] - 2026-10-20 diff --git a/OWNd/message/heating.py b/OWNd/message/heating.py index a9a38f9..3caeac3 100644 --- a/OWNd/message/heating.py +++ b/OWNd/message/heating.py @@ -1328,17 +1328,24 @@ def set_timed_manual_end_time( @classmethod def central_status(cls, where: str = "#0") -> OWNHeatingCommand: - """Deprecated: builds ``*#4*WHERE*14##``, which gateways refuse. - - A real MyHomeServer1 3.1.8 (OWNd#77 gateway probe) answers ``*#*0##`` - (NACK); on an F454 (MyHOME#629) dimension 14 silently timed out while - ``status("#0")`` (``*#4*#0##``) answered within 0.13 s. Use - ``status(where)`` to poll a central unit. The frame is kept unchanged - for existing callers. + """Deprecated: builds ``*#4*WHERE*14##``, unanswered without a central unit. + + In the firmware oracle (``trace-corpus``) F453, F453AV, F460, F461, + MH200N and MyHomeServer1 forward ``*#4*#0*14##`` to the bus + (``nack``/``out``), while F450, F454, F459, H4684, MH200 and MH202 + refuse it (``nack``/``silent``). Forwarded or not, only a central unit + answers it: a live F461 2.0.11 without one NACKs it after the bus + timeout, a live MyHomeServer1 3.87.13 without one NACKs it after + ~4.2 s, a real MyHomeServer1 3.1.8 (OWNd#77 gateway probe) NACKs it, + and on an F454 (MyHOME#629) it timed out while ``status("#0")`` + (``*#4*#0##``) answered within 0.13 s. Use ``status('#0')`` (or + ``status(where)``) to poll a central unit. The frame is kept + unchanged for existing callers. """ warnings.warn( - "OWNHeatingCommand.central_status() builds *#4*WHERE*14##, which gateways " - "refuse; use OWNHeatingCommand.status(where) instead", + "OWNHeatingCommand.central_status() builds *#4*WHERE*14##, which goes " + "unanswered without a central unit; use OWNHeatingCommand.status('#0') " + "(OWNHeatingCommand.status(where)) instead", DeprecationWarning, stacklevel=2, ) diff --git a/tests/test_who4_central_status_text.py b/tests/test_who4_central_status_text.py new file mode 100644 index 0000000..6fe2301 --- /dev/null +++ b/tests/test_who4_central_status_text.py @@ -0,0 +1,39 @@ +"""``central_status`` deprecation text names the real reason. + +``*#4*#0*14##`` is forwarded to the bus by F453, F453AV, F460, F461, MH200N +and MyHomeServer1 in the firmware oracle (``trace-corpus``, ``nack``/``out``) +and refused by the other six; either way it goes unanswered without a +central unit. The frame is unchanged. +""" + +from __future__ import annotations + +import warnings + +from OWNd.message import OWNHeatingCommand + + +def test_central_status_deprecation_names_the_real_reason() -> None: + doc = OWNHeatingCommand.central_status.__doc__ or "" + assert "which gateways refuse" not in doc + assert "unanswered without a central unit" in doc + assert "trace-corpus" in doc + assert "F461" in doc + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + command = OWNHeatingCommand.central_status("#0") + assert str(command) == "*#4*#0*14##" + (warning,) = caught + assert issubclass(warning.category, DeprecationWarning) + text = str(warning.message) + assert "refuse" not in text + assert "unanswered without a central unit" in text + assert "status('#0')" in text + + +def test_central_status_frame_and_default_are_unchanged() -> None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + assert str(OWNHeatingCommand.central_status()) == "*#4*#0*14##" + assert str(OWNHeatingCommand.central_status("#0#1")) == "*#4*#0#1*14##" From 8194705129bff53c8528d5394982e5c239b580f1 Mon Sep 17 00:00:00 2001 From: Mac Book Date: Mon, 26 Oct 2026 22:12:05 +0200 Subject: [PATCH 2/2] docs(heating): central_status docstring says *#4*#0*14## is undefined in the WHO 4 document The Legrand WHO 4 document reads dimension 14 on zones 1..99 only (p. 18) and, on #0, dimensions 30 and 31 (pp. 57-58); the central unit's set point comes back inside the *4*110#T*#0## reply to *#4*#0## (p. 56). Text only. Co-Authored-By: Claude Fable 5.1 --- OWNd/message/heating.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/OWNd/message/heating.py b/OWNd/message/heating.py index 3caeac3..a5476ed 100644 --- a/OWNd/message/heating.py +++ b/OWNd/message/heating.py @@ -1338,9 +1338,13 @@ def central_status(cls, where: str = "#0") -> OWNHeatingCommand: timeout, a live MyHomeServer1 3.87.13 without one NACKs it after ~4.2 s, a real MyHomeServer1 3.1.8 (OWNd#77 gateway probe) NACKs it, and on an F454 (MyHOME#629) it timed out while ``status("#0")`` - (``*#4*#0##``) answered within 0.13 s. Use ``status('#0')`` (or - ``status(where)``) to poll a central unit. The frame is kept - unchanged for existing callers. + (``*#4*#0##``) answered within 0.13 s. The Legrand WHO 4 document + defines the dimension 14 read on zones 1..99 only (p. 18) and, on + ``#0``, reads of dimensions 30 and 31 (pp. 57-58): ``*#4*#0*14##`` is + undefined there, and the central unit's set point comes back inside + the ``*4*110#T*#0##`` reply to ``*#4*#0##`` (p. 56). Use + ``status('#0')`` (or ``status(where)``) to poll a central unit. The + frame is kept unchanged for existing callers. """ warnings.warn( "OWNHeatingCommand.central_status() builds *#4*WHERE*14##, which goes "