Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 53 additions & 20 deletions OWNd/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
),
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions tests/test_h4684_gateway_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 36 additions & 2 deletions tests/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand Down
Loading