diff --git a/OWNd/profiles.py b/OWNd/profiles.py index c255a47..a31485c 100644 --- a/OWNd/profiles.py +++ b/OWNd/profiles.py @@ -188,6 +188,10 @@ def can_support_workers(self, count: int) -> bool: # Firmware at which Legrand documents CEN+ on the F453AV (WHO 25 p. 13). _F453AV_CEN_PLUS_FIRMWARE = (2, 1, 7) +# Firmware generation at which physical H/L4684 captures show WHO 22 (bt_multi) +# alongside WHO 16 (MyHOME#466 comment 6398442412: FW 6.0.11, distro 6.0.0). +_H4684_WHO22_FIRMWARE = (6, 0, 0) + class F452Profile(GatewayProfile): """The F452 Web Server. @@ -572,31 +576,56 @@ def __init__(self) -> None: class H4684Profile(GatewayProfile): - """The H4684 colour touch screen family (H4684, L4684, N4684, NT4684, LGRH4684). - - Verified in the 22-suite firmware oracle against H4684 firmware 2.0.54 - (``*#13**15*13##``, ``*#13**16*2*0*54##``, ``*#13**23*2*0*5##``, - ``*#13**24*3*0*0##``, ``*#1013**1*19##``): - - Ships ``bt_autom``, ``bt_termoreg``, ``bt_energia`` and ``bt_difson`` - (WHO 1, 2, 3, 4, 15, 16, 18; ``*#16*0*5##`` answers ``ack``/``out`` in - ``sound-who16-source.tsv``), with legacy OPEN password authentication - in ``openserver``. - - Does not ship ``bt_multi`` or ``bt_supervisione``, and NACKs all WHO 17 - (``scenario-who17``), WHO 22 (``sound-who22``), and WHO 25 - (``who25-messages``) frames, so WHO 17, 22, and 25 are excluded. + """The H4684 colour touch screen family (H4684, H/L4684, L4684, N4684, NT4684, LGRH4684). + + Two firmware generations are evidenced: + 1. Emulated H4684 firmware 2.0.54 in the 22-suite firmware oracle + (``*#13**15*13##``, ``*#13**16*2*0*54##``, ``*#13**23*2*0*5##``, + ``*#13**24*3*0*0##``, ``*#1013**1*19##``): + - Ships ``bt_autom``, ``bt_termoreg``, ``bt_energia`` and ``bt_difson`` + (WHO 1, 2, 3, 4, 15, 16, 18; ``*#16*0*5##`` answers ``ack``/``out`` in + ``sound-who16-source.tsv``), with legacy OPEN password authentication + in ``openserver``. + - Does not ship ``bt_multi`` or ``bt_supervisione``, and NACKs all WHO 17 + (``scenario-who17``), WHO 22 (``sound-who22``), and WHO 25 + (``who25-messages``) frames. + 2. Physical H/L4684 firmware 6.0.11 (MyHOME#466 comment 6398442412: UPnP + ``modelName`` ``H/L4684``, ``*#13**15*23##``, ``*#13**16*6*0*11##``, + ``*#13**23*2*6*2##``, ``*#13**24*6*0*0##``): + - Ships ``bt_multi`` alongside ``bt_difson``, translating ``*#16*0*5##`` + into dual-dialect WHO 16 and WHO 22 event-session inventory + (``*#22*3#1#1*12*0*10##``, ``*#22*2#1*12*1*4##``). + - Behaviour between 2.0.54 and 6.0.0 is uncaptured, so firmware below + 6.0.0 or unknown firmware conservatively omits WHO 22. """ - def __init__(self) -> None: + def __init__( + self, + firmware_version: str | tuple[int, ...] | list[int] | None = None, + ) -> None: + if not firmware_version: + fw_str = None + elif isinstance(firmware_version, (list, tuple)): + fw_str = ".".join(str(p) for p in firmware_version) + else: + fw_str = str(firmware_version) + base_who = ( + WHO_LIGHTING, + WHO_AUTOMATION, + WHO_LOAD_CONTROL, + WHO_HEATING, + WHO_CEN, + WHO_SOUND, + WHO_ENERGY, + ) + has_who22 = ( + parse_firmware_version(firmware_version) >= _H4684_WHO22_FIRMWARE + ) super().__init__( model_name="H4684", + firmware_version=fw_str, supported_who=( - WHO_LIGHTING, - WHO_AUTOMATION, - WHO_LOAD_CONTROL, - WHO_HEATING, - WHO_CEN, - WHO_SOUND, - WHO_ENERGY, + (*base_who, WHO_SOUND_DIFFUSION) if has_who22 else base_who ), ) @@ -684,6 +713,8 @@ def canonical_profiles() -> tuple[GatewayProfile, ...]: "ln4890a": "h4890", "4890": "h4890", "l4684": "h4684", + "hl4684": "h4684", + "hc4684": "h4684", "n4684": "h4684", "nt4684": "h4684", "lgrh4684": "h4684", @@ -723,6 +754,8 @@ def get_gateway_profile( normalized = _ALIASES.get(normalized, normalized) if normalized == "f453av": return F453AVProfile(firmware_version=firmware_version) + if normalized == "h4684": + return H4684Profile(firmware_version=firmware_version) profile = _PROFILES.get(normalized) if profile is not None: return profile diff --git a/tests/test_h4684_gateway_trace.py b/tests/test_h4684_gateway_trace.py index 5c0e96c..28c2819 100644 --- a/tests/test_h4684_gateway_trace.py +++ b/tests/test_h4684_gateway_trace.py @@ -145,3 +145,37 @@ def test_h4684_qemu_oracle_trace_corpus_outputs(): assert isinstance(evt_diag, OWNGatewayDiagnosticEvent) assert evt_diag.device_type == "H4684" assert evt_diag.object_model == "19" + + +def test_hl4684_physical_fw6_0_11_trace_sequence(): + """Replay physical H/L4684 FW 6.0.11 identification and profile upgrade (MyHOME#466).""" + from OWNd.connection import OWNGateway + from OWNd.profiles import WHO_SOUND, WHO_SOUND_DIFFUSION + + gw = OWNGateway({"address": "192.0.2.10", "modelName": "H/L4684"}) + assert isinstance(gw.profile, H4684Profile) + assert gw.profile.model_name == "H4684" + assert not gw.profile.supports_who(WHO_SOUND_DIFFUSION) + + evt_dev = OWNEvent.parse("*#13**15*23##") + assert isinstance(evt_dev, OWNGatewayEvent) + assert evt_dev.device_type == "H4684" + assert evt_dev.device_type_code == "23" + + evt_fw = OWNEvent.parse("*#13**16*6*0*11##") + assert isinstance(evt_fw, OWNGatewayEvent) + assert evt_fw.firmware_version == "6.0.11" + + evt_kernel = OWNEvent.parse("*#13**23*2*6*2##") + assert isinstance(evt_kernel, OWNGatewayEvent) + assert evt_kernel.kernel_version == "2.6.2" + + evt_distro = OWNEvent.parse("*#13**24*6*0*0##") + assert isinstance(evt_distro, OWNGatewayEvent) + assert evt_distro.distribution_version == "6.0.0" + + gw.firmware = evt_fw.firmware_version + assert isinstance(gw.profile, H4684Profile) + assert gw.profile.firmware_version == "6.0.11" + assert gw.profile.supports_who(WHO_SOUND) is True + assert gw.profile.supports_who(WHO_SOUND_DIFFUSION) is True diff --git a/tests/test_profiles.py b/tests/test_profiles.py index 83ca9fc..2237725 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -599,9 +599,23 @@ def test_profile_no_auth_features() -> None: assert "HMAC-SHA2" not in profile.features_summary -@pytest.mark.parametrize("name", ["H4684", "h4684", "L4684", "N4684", "NT4684", "LGRH4684", "LGR4684"]) +@pytest.mark.parametrize( + "name", + [ + "H4684", + "h4684", + "H/L4684", + "HL4684", + "HC4684", + "L4684", + "N4684", + "NT4684", + "LGRH4684", + "LGR4684", + ], +) def test_h4684_profile(name: str) -> None: - """H4684 colour touch screen and brand variants resolve to H4684Profile.""" + """H4684 colour touch screen, UPnP H/L4684 spelling, and brand variants resolve to H4684Profile.""" profile = get_gateway_profile(name) assert isinstance(profile, H4684Profile) assert profile.model_name == "H4684" @@ -620,6 +634,26 @@ def test_h4684_profile(name: str) -> None: assert not profile.supports_who(WHO_CEN_PLUS) +def test_h4684_profile_firmware_gated_who22() -> None: + """Physical H/L4684 firmware 6.x ships bt_multi (WHO 22), while 2.x omits it.""" + fw2 = get_gateway_profile("H/L4684", "2.0.54") + assert isinstance(fw2, H4684Profile) + assert fw2.firmware_version == "2.0.54" + assert not fw2.supports_who(WHO_SOUND_DIFFUSION) + + fw6 = get_gateway_profile("H/L4684", "6.0.11") + assert isinstance(fw6, H4684Profile) + assert fw6.firmware_version == "6.0.11" + assert fw6.supports_who(WHO_SOUND) is True + assert fw6.supports_who(WHO_SOUND_DIFFUSION) is True + assert not fw6.supports_who(WHO_SCENARIO) + assert not fw6.supports_who(WHO_CEN_PLUS) + + fw6_tuple = H4684Profile(firmware_version=(6, 0, 0)) + assert fw6_tuple.firmware_version == "6.0.0" + assert fw6_tuple.supports_who(WHO_SOUND_DIFFUSION) is True + + def test_profile_injection_capability_flags() -> None: """Only the F461 has injection evidence; every other family is unverified.""" f461 = get_gateway_profile("F461")