diff --git a/CHANGELOG.md b/CHANGELOG.md index 655b70d..ced0528 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. +- **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 c255a47..edd767d 100644 --- a/OWNd/profiles.py +++ b/OWNd/profiles.py @@ -185,6 +185,17 @@ 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"). Their sound system is WHO 22 (sound-who22), so supports_audio stays +# True and WHO 22 stays listed. +_WITHOUT_WHO16 = (WHO_SOUND,) + # Firmware at which Legrand documents CEN+ on the F453AV (WHO 25 p. 13). _F453AV_CEN_PLUS_FIRMWARE = (2, 1, 7) @@ -387,6 +398,8 @@ 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, the sound system is WHO 22 (see + ``_WITHOUT_WHO16``). 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 @@ -415,7 +428,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 ), ) @@ -532,6 +547,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: @@ -610,6 +630,8 @@ 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, the sound system is WHO 22 (see + ``_WITHOUT_WHO16``). """ def __init__(self) -> None: @@ -624,7 +646,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 83ca9fc..9ae33f1 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) @@ -651,3 +653,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)