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
7 changes: 4 additions & 3 deletions custom_components/myhome/config_flow_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from homeassistant.helpers import device_registry as dr
from OWNd.connection import OWNCommandSession
from OWNd.message import OWNGatewayCommand, OWNGatewayDiagnosticCommand
from OWNd.profiles import get_gateway_profile
from voluptuous import All, Coerce, In, Range, Required

from .const import (
Expand All @@ -38,7 +37,7 @@
DOMAIN,
LOGGER,
)
from .gateway import command_session_default
from .gateway import _profile_for_model, command_session_default
from .identity import (
UNTRUSTED_MODEL_LABELS,
CodeReading,
Expand Down Expand Up @@ -654,7 +653,9 @@ async def _async_apply_gateway_identity(self, gateway: typing.Any) -> str | None
configured,
)
gateway.model_name = gateway.model = model
gateway.profile = get_gateway_profile(model, gateway.model_number)
# The firmware is not known yet (the runtime re-resolves with it at connect time);
# the same helper as the runtime, so SSDP spellings map the same way.
gateway.profile = _profile_for_model(model)
gateway._log_id = f"[{model} gateway - {gateway.host}]" # pylint: disable=protected-access
return resolution.source

Expand Down
20 changes: 10 additions & 10 deletions custom_components/myhome/legacy_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ def canonicalise_imported_platforms(
``where``, the ``members`` and the device key are rewritten in a new copy (the
stored one is not touched). When the copy already has a device under the canonical
key, that one stays and the 4-digit twin is dropped with a WARNING. Returns the new
copy and one ``plat: old -> new`` line per rewritten device.
copy and one line per rewritten device: ``plat: old -> new`` for a moved key, or
``plat: key members [old] -> [new]`` when only a scope cover's members moved.
"""
rewritten: list[str] = []
result: dict[str, dict[str, Any]] = {}
Expand Down Expand Up @@ -156,7 +157,10 @@ def canonicalise_imported_platforms(
)
continue
new_devices[new_id] = new_cfg
rewritten.append(f"{plat}: {d_id} -> {new_id}")
if new_id != d_id:
rewritten.append(f"{plat}: {d_id} -> {new_id}")
else:
rewritten.append(f"{plat}: {d_id} members {members} -> {new_members}")
result[plat] = new_devices
return result, rewritten

Expand Down Expand Up @@ -227,15 +231,11 @@ def _apply_imported_copy(
def _same_mac(first: Any, second: Any) -> bool:
"""Whether two MAC spellings (colons, hyphens, dots, raw hex) name the same gateway.

Diagnostic path only: when ``dr.format_mac`` rejects a value the two are compared
as plain strings, so a malformed section key keeps the orphan WARNING instead of
raising into the generic "Failed to parse" error of ``_load_live_yaml``.
``dr.format_mac`` never raises: a value that is no MAC comes back as it went in, so two
equal malformed strings still compare equal. Such a section key never gets this far
anyway: ``validate.config_schema`` rejects it first with the "Failed to parse" error.
"""
first_text, second_text = str(first), str(second)
try:
return dr.format_mac(first_text) == dr.format_mac(second_text)
except Exception:
return first_text == second_text
return dr.format_mac(str(first)) == dr.format_mac(str(second))


def _log_mac_mismatch(hass: HomeAssistant, entry: ConfigEntry, keys: list[Any]) -> None:
Expand Down
6 changes: 4 additions & 2 deletions scripts/update_gateway_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,10 @@ def build_options_block() -> str:
def discovery_whos_for(profile: object) -> list[int]:
"""The startup discovery WHOs the runtime sends to a gateway with ``profile``.

Mirrors ``MyHOMEGatewayHandler._profile_supports_who``: a WHO is queried only when
the OWNd profile advertises it.
The same rule as ``MyHOMEGatewayHandler._profile_supports_who`` for a real OWNd
profile: a WHO is queried only when the profile advertises it. The two differ at the
edges: an object without ``supports_who`` yields no rows here (the table lists what a
profile advertises), while the runtime gate assumes everything and queries every WHO.
"""
supports = getattr(profile, "supports_who", None)
if not callable(supports):
Expand Down
8 changes: 8 additions & 0 deletions tests/test_canonical_point_where.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,14 @@ def test_imported_copy_is_rewritten_to_the_canonical_spelling(caplog) -> None:
assert again == result and rewritten_again == []


def test_imported_copy_reports_a_members_only_rewrite_as_such() -> None:
"""A scope cover under a canonical key whose `members` hold `0004`: the line names the members, not a key move."""
snapshot = {"cover": {"2-1": {"who": "2", "where": "1", "members": ["0004", "12"], "name": "Area 1", "entities": {}}}}
result, rewritten = canonicalise_imported_platforms(snapshot)
assert result["cover"]["2-1"]["members"] == ["04", "12"]
assert rewritten == ["cover: 2-1 members ['0004', '12'] -> ['04', '12']"]


def test_imported_copy_keeps_the_canonical_device_on_a_collision(caplog) -> None:
"""A copy holding both `1-0004` and `1-04` keeps `1-04`; the twin is dropped, not overwritten."""
snapshot = {"light": {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_gateway_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def test_oracle_check_rejects_a_sweep_omission_the_profile_does_not_advertise(mo


def test_discovery_whos_follow_the_runtime_gate():
"""``discovery_whos_for`` mirrors ``MyHOMEGatewayHandler._profile_supports_who`` over the discovery WHOs."""
"""``discovery_whos_for`` follows ``_profile_supports_who`` for real profiles; without ``supports_who`` it yields no rows (the runtime gate assumes everything)."""
from OWNd.profiles import get_gateway_profile

for name in ("F454", "F455", "F461", "MH200N", "MH202", "MyHomeServer1"):
Expand Down
49 changes: 18 additions & 31 deletions tests/test_legacy_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from custom_components.myhome.const import (
CONF_FILE_PATH,
Expand Down Expand Up @@ -468,50 +467,38 @@ async def test_legacy_yaml_other_gateway_section_spelled_differently_does_not_wa
assert _myhome_warnings(caplog) == []


async def test_legacy_yaml_mac_mismatch_keeps_warning_when_format_mac_raises(
def test_same_mac_compares_spellings_and_never_raises():
"""`dr.format_mac` returns a non-MAC unchanged, so no fallback is needed: equal junk is equal, a MAC is not junk."""
assert _same_mac("00-03-50-99-99-99", "00:03:50:99:99:99") is True
assert _same_mac("0003.5099.9999", "00:03:50:99:99:99") is True
assert _same_mac("not-a-mac", "not-a-mac") is True
assert _same_mac("not-a-mac", "00:03:50:99:99:99") is False
assert _same_mac(None, "None") is True


async def test_legacy_yaml_malformed_section_key_is_a_parse_error_not_a_mismatch(
mock_hass, base_entry, tmp_path, caplog
):
"""A key dr.format_mac rejects is still reported at WARNING, not as a parse ERROR."""
"""A section key that is no MAC is refused by the schema before the mismatch check could run."""
yaml_file = tmp_path / "myhome.yaml"
yaml_file.write_text(
"'00:03:50:00:00:00':\n light:\n hallway:\n where: '21'\n name: 'Hallway'\n",
"'not-a-mac':\n light:\n hallway:\n where: '21'\n name: 'Hallway'\n",
encoding="utf-8",
)
mock_hass.config_entries.async_entries.return_value = [
base_entry,
_other_entry("00:03:50:99:99:99"),
]
mock_hass.config_entries.async_entries.return_value = [base_entry]
configured_platforms = _make_configured_platforms()
real_format_mac = dr.format_mac

def _raising_format_mac(value: str) -> str:
if value != base_entry.data[CONF_MAC]:
raise ValueError(f"malformed MAC {value!r}")
return real_format_mac(value)

with (
caplog.at_level("WARNING", logger="custom_components.myhome"),
patch("custom_components.myhome.legacy_yaml.dr.format_mac", _raising_format_mac),
patch("custom_components.myhome.legacy_yaml._log_mac_mismatch") as mismatch,
):
await load_legacy_myhome_yaml(mock_hass, base_entry, configured_platforms)

mismatch.assert_not_called()
assert configured_platforms["light"] == {}
assert not [r for r in caplog.records if r.levelname == "ERROR"]
warnings = _myhome_warnings(caplog)
assert len(warnings) == 1, warnings
assert "00:03:50:81:22:33" in warnings[0]
assert "00:03:50:00:00:00" in warnings[0]


def test_same_mac_falls_back_to_string_comparison_when_format_mac_raises():
"""The helper compares raw strings when dr.format_mac raises, and never raises itself."""

def _raising_format_mac(value: str) -> str:
raise ValueError(f"malformed MAC {value!r}")

with patch("custom_components.myhome.legacy_yaml.dr.format_mac", _raising_format_mac):
assert _same_mac("not-a-mac", "not-a-mac") is True
assert _same_mac("not-a-mac", "00:03:50:99:99:99") is False
errors = [r.getMessage() for r in caplog.records if r.levelname == "ERROR"]
assert len(errors) == 1 and "Invalid MAC address" in errors[0]
assert _myhome_warnings(caplog) == []


async def test_legacy_yaml_mac_mismatch_skips_other_entry_without_mac(
Expand Down
Loading