diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a75d5..4c7778d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to `OWNd` are documented in this file. - **`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 central-unit builders (`OWNHeatingCommand.set_central_mode`, `set_central_temperature`, `set_central_program`, `set_central_scenario`, `set_central_weekend`, `set_central_holiday`, `set_holiday_end_date`, `set_holiday_end_time`, `set_timed_manual_end_time`)**: one shared `_validate_central_where` guard raises `ValueError` unless `where` is the central unit itself: `#0` (3550, 99 zones) or `#0#C` with `C` the 4695 unit's configured zone 1..99 or `01`..`99` (the forms MyHOME and the golden corpus use; whitespace is stripped, the stripped value is quoted). A bare zone (`*4*3101*1##`, `*#4*1*#30*...##` used to be built), a zone through the central unit (`#Z`, `#Z#C`: libqtdevices `ControlledProbeDevice` addresses, bus command byte `C2`, built by `set_mode`; the error names it), `#00`, `#0#0`, `#0#00` and three-digit values are refused. The plain zone form `*4*311*Z##` is `nack`/`silent` on all 12 emulated gateways (own-firmware-oracle `thermo-zone-mode`, `ownd-pr82-heating`) and on a live F461 2.0.11. - **WHO 4 (`OWNHeatingEvent`)**: a Dimension 14 setpoint reply with a `#`-prefixed value (`*#4*#1*14*#0230*1##`, `MyHOME#139`) parses as a 23.0 °C target temperature instead of `None`; `dimension_value` keeps `#0230`. +- **Profiles (`F461Profile`, `MyHomeServer1Profile`)**: WHO 16 is no longer in `supported_who`: all 86 WHO 16 frames of the firmware oracle are `nack`/`silent` on F461 020011 and MyHomeServer1 028206 (`sound-who16-source` and the other suites), a live F461 2.0.11 NACKs `*#16*0*5##`, and the MyHomeServer1 firmware discards WHO 16 (`MyHOME#459`). WHO 22 stays listed and `supports_audio` is unchanged; MH202 keeps WHO 16. ## [2.0.0b11] - 2026-10-20 diff --git a/OWNd/profiles.py b/OWNd/profiles.py index a31485c..511d482 100644 --- a/OWNd/profiles.py +++ b/OWNd/profiles.py @@ -185,6 +185,20 @@ def can_support_workers(self, count: int) -> bool: who for who in DEFAULT_SUPPORTED_WHO if who != WHO_CEN_PLUS ) +# Gateways without a WHO 16 translator: the F461 020011 and MyHomeServer1 +# 028206 images ship bt_multi (WHO 22) but no bt_difson (WHO 16) +# (own-firmware-oracle README firmware table; the F461 openserver Stackopen +# lists no WHO 16 client), every WHO 16 frame of the firmware oracle (86 per +# gateway, sound-who16-source and the other suites) is nack/silent on them, a +# live F461 2.0.11 NACKs *#16*0*5## within 5 ms, and a MyHomeServer1 NACKs it +# in the MyHOME issue_466 trace fixture (MyHOME#459: "firmware discards 5, 16, +# 22"). WHO 22 is out of scope here and left as it is: in the oracle the +# 028206 image's bt_multi translates WHO 22 commands onto the bus +# (sound-who22, ack/out), so supports_audio stays True and WHO 22 stays +# listed; whether a live MyHomeServer1 3.87.x routes WHO 22 on a production +# plant is not settled by this change (MyHOME's topology strips 22 on it). +_WITHOUT_WHO16 = (WHO_SOUND,) + # Firmware at which Legrand documents CEN+ on the F453AV (WHO 25 p. 13). _F453AV_CEN_PLUS_FIRMWARE = (2, 1, 7) @@ -391,6 +405,11 @@ class F461Profile(GatewayProfile): (`020011`) does not package the ``bt_energia`` translator and strictly NACKs all WHO 3 requests without bus activity (`results/F461/020011/oracle/full/energy-ts10.tsv`). WHO 3 is therefore excluded from ``supported_who``. + - WHO 16 (Sound System): refused (no ``bt_difson``, every WHO 16 frame + nack/silent in the oracle and on a live plant; see ``_WITHOUT_WHO16``). + WHO 22 is listed because the emulated image translates it onto the bus + (``sound-who22``); live routing on production plants is not measured + here. Injection flags, from the live F461 2.0.11 capture of 2026-10-18 (MyHOME ``tests/fixtures/traces/f461_live_2026-10-18``) and the plant @@ -419,7 +438,9 @@ def __init__(self) -> None: supports_who4_command_injection=True, supports_who18_response_injection=True, supported_who=tuple( - w for w in DEFAULT_SUPPORTED_WHO if w != WHO_LOAD_CONTROL + w + for w in DEFAULT_SUPPORTED_WHO + if w != WHO_LOAD_CONTROL and w not in _WITHOUT_WHO16 ), ) @@ -536,6 +557,11 @@ class MH202Profile(GatewayProfile): *5*9*0##) and its zone states (WHAT 11/18). WHO 5 here means reading the alarm: the plant owner reports the central unit rejects SCS arm/disarm from any gateway and arms through WHO 9 AUX frames instead (see OWNAlarmCommand). + + WHO 16 stays listed although the firmware oracle (MH202 010024) refuses + every WHO 16 command: a live MH202 with an F441M matrix relays WHO 16 + state frames (``*#16*41*1*1##``, ``*#16*41*1*6##``; MyHOME#669 trace), + and ``supported_who`` also decides which gateway owns those events. """ def __init__(self) -> None: @@ -639,6 +665,13 @@ class MyHomeServer1Profile(GatewayProfile): NACKs all WHO 3 requests without bus activity (see own-firmware-oracle findings/MyHomeServer1/energy-ts10.md). WHO 3 is therefore excluded from ``supported_who``. + - WHO 16 (Sound System): refused (no ``bt_difson``, every WHO 16 frame + nack/silent in the oracle and on a live plant; see ``_WITHOUT_WHO16``). + WHO 22 is listed because the emulated 028206 image's ``bt_multi`` + translates it onto the bus (``sound-who22``); whether a live + MyHomeServer1 3.87.x routes WHO 22 on a production plant is not + measured here (MyHOME#459 reports it discarded; MyHOME's topology + strips 22 on it). """ def __init__(self) -> None: @@ -653,7 +686,9 @@ def __init__(self) -> None: supports_native_transitions=True, supports_extended_frames=True, supported_who=tuple( - w for w in DEFAULT_SUPPORTED_WHO if w != WHO_LOAD_CONTROL + w + for w in DEFAULT_SUPPORTED_WHO + if w != WHO_LOAD_CONTROL and w not in _WITHOUT_WHO16 ), ) diff --git a/tests/test_profiles.py b/tests/test_profiles.py index 2237725..74caf21 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -103,7 +103,9 @@ def test_profile_lookup_accepts_common_name_variants() -> None: assert profile.model_name == "MyHomeServer1" assert profile.supports_session_count(4) - assert profile.supports_who(WHO_SOUND) + # Firmware oracle: every WHO 16 frame is refused; sound is WHO 22. + assert not profile.supports_who(WHO_SOUND) + assert profile.supports_who(WHO_SOUND_DIFFUSION) # Firmware oracle proves MyHomeServer1 omits bt_energia and rejects WHO 3 assert not profile.supports_who(WHO_LOAD_CONTROL) @@ -685,3 +687,27 @@ def test_profile_injection_capability_flags() -> None: assert p.supports_who18_response_injection is None +@pytest.mark.parametrize("model", ["F461", "MyHomeServer1", "MHS1", "MyHome Server 1"]) +def test_who16_is_not_advertised_where_the_oracle_refuses_it(model: str) -> None: + """Every WHO 16 frame of the oracle (86 per gateway, sound-who16-source and + others) is ``nack``/``silent`` on F461 020011 and MyHomeServer1 028206: no + translator forwards it; a live F461 2.0.11 NACKs ``*#16*0*5##`` within + 5 ms and a MyHomeServer1 NACKs it in the MyHOME issue_466 trace fixture + (MyHOME#459). They forward WHO 22 (``sound-who22``), so sound stays + advertised through WHO 22 and ``supports_audio``. + """ + profile = get_gateway_profile(model) + + assert not profile.supports_who(WHO_SOUND) + assert profile.supports_who(WHO_SOUND_DIFFUSION) + assert profile.supports_audio is True + + +@pytest.mark.parametrize( + "model", ["F454", "MH200N", "MH200", "H4684", "F453", "F453AV", "H4890", "MH202"] +) +def test_who16_stays_on_gateways_that_forward_it(model: str) -> None: + """MH202: the oracle refuses WHO 16 commands, but a live MH202 relays WHO 16 + state frames from an F441M (``*#16*41*1*1##``, MyHOME#669), so it keeps + owning WHO 16 events.""" + assert get_gateway_profile(model).supports_who(WHO_SOUND)