diff --git a/custom_components/myhome/config_flow_discovery.py b/custom_components/myhome/config_flow_discovery.py index 3a3ac84c5..8928ee3a1 100644 --- a/custom_components/myhome/config_flow_discovery.py +++ b/custom_components/myhome/config_flow_discovery.py @@ -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 ( @@ -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, @@ -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 diff --git a/custom_components/myhome/legacy_yaml.py b/custom_components/myhome/legacy_yaml.py index 453747902..8663fbe8a 100644 --- a/custom_components/myhome/legacy_yaml.py +++ b/custom_components/myhome/legacy_yaml.py @@ -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]] = {} @@ -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 @@ -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: diff --git a/scripts/update_gateway_profiles.py b/scripts/update_gateway_profiles.py index 53cd2fa21..56cf23d4a 100644 --- a/scripts/update_gateway_profiles.py +++ b/scripts/update_gateway_profiles.py @@ -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): diff --git a/tests/test_canonical_point_where.py b/tests/test_canonical_point_where.py index f05f193fe..dc9087dde 100644 --- a/tests/test_canonical_point_where.py +++ b/tests/test_canonical_point_where.py @@ -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": { diff --git a/tests/test_gateway_profiles.py b/tests/test_gateway_profiles.py index 8865c6296..09e2cd2b6 100644 --- a/tests/test_gateway_profiles.py +++ b/tests/test_gateway_profiles.py @@ -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"): diff --git a/tests/test_legacy_yaml.py b/tests/test_legacy_yaml.py index 8cbf8a901..543189e72 100644 --- a/tests/test_legacy_yaml.py +++ b/tests/test_legacy_yaml.py @@ -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, @@ -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(