diff --git a/custom_components/nvidia_shield/config_flow.py b/custom_components/nvidia_shield/config_flow.py index 728918a..bac1f18 100644 --- a/custom_components/nvidia_shield/config_flow.py +++ b/custom_components/nvidia_shield/config_flow.py @@ -6,6 +6,7 @@ import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers import entity_registry as er from homeassistant.helpers import selector @@ -51,6 +52,34 @@ def _get_receiver_inputs(hass, receiver_entity_id: str | None) -> list[str]: return list(input_map.keys()) if isinstance(input_map, dict) else [] +def _unique_id_from_atv_device(hass, androidtv_entity: str) -> str | None: + """A stable unique_id from the atv entity's device MAC (t29 G2). + + The device registry holds CONNECTION_NETWORK_MAC for Android TV + entities configured with a host — far stronger than the renameable + entity_id. None (→ no unique_id at all, not an entity-derived + placeholder) when the entity is unregistered or carries no MAC. + """ + if not androidtv_entity: + return None + registry = er.async_get(hass) + entity = registry.entities.get(androidtv_entity) + device_id = getattr(entity, "device_id", None) if entity else None + device = ( + dr.async_get(hass).async_get(device_id) if device_id else None + ) + if device is None: + return None + if isinstance(dr, dict): # stub shape guard + return None + for connection in device.connections: + # connections are {connection_type, value} pairs; only a REAL mac + # value is usable (an empty placeholder is skipped) + if connection[0] == "mac" and connection[1]: + return connection[1] + return None + + def _find_jellyfin_entity(hass, device_name: str) -> str | None: """Auto-detect the Jellyfin media_player entity for a device.""" registry = er.async_get(hass) @@ -214,8 +243,18 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - if androidtv_entity: - await self.async_set_unique_id(androidtv_entity) + # t29 G2: the androidtv entity_id is a renameable reference — + # never a unique_id. A sqlite/registry MAC is preferred (the + # atv entity's device holds CONNECTION_NETWORK_MAC); without a + # resolvable MAC the entry gets NO unique_id (the same + # uniqueness-less semantics _async_handle_discovery_without_ + # unique_id applies to MAC-less discovery) — renames can never + # break uniqueness. + fallback_unique_id = _unique_id_from_atv_device( + self.hass, androidtv_entity + ) + if fallback_unique_id: + await self.async_set_unique_id(fallback_unique_id) self._abort_if_unique_id_configured() # Auto-detect Jellyfin if not manually specified diff --git a/tests/entity_stublib.py b/tests/entity_stublib.py index 8f99814..714b7b4 100644 --- a/tests/entity_stublib.py +++ b/tests/entity_stublib.py @@ -24,6 +24,7 @@ ENTITIES_STUB_NAMES = ( "homeassistant.components.button", "homeassistant.helpers", "homeassistant.helpers.entity_registry", + "homeassistant.helpers.device_registry", "homeassistant.helpers.selector", "homeassistant.helpers.event", "homeassistant.helpers.entity", @@ -253,9 +254,26 @@ def install() -> None: return None entity_registry.async_get = lambda hass: _Registry() + device_registry = types.ModuleType("homeassistant.helpers.device_registry") + + class _DeviceRegistry: + def __init__(self): + self.devices: dict[str, object] = {} + + def async_get(self, device_id): + return self.devices.get(device_id) + + _device_registry_shared = _DeviceRegistry() + + device_registry.async_get = lambda hass: _device_registry_shared + sys.modules["homeassistant.helpers.device_registry"] = device_registry + helpers.entity_registry = entity_registry sys.modules["homeassistant.helpers.entity_registry"] = entity_registry + helpers.device_registry = device_registry + sys.modules["homeassistant.helpers.device_registry"] = device_registry + event = types.ModuleType("homeassistant.helpers.event") event.async_call_later = lambda hass, delay, callback_fn: (lambda: None) event.async_track_state_change_event = lambda *args, **kwargs: (lambda: None) diff --git a/tests/test_config_flow_pairing.py b/tests/test_config_flow_pairing.py index b17ede4..f54257d 100644 --- a/tests/test_config_flow_pairing.py +++ b/tests/test_config_flow_pairing.py @@ -35,6 +35,7 @@ _STUB_NAMES = ( "homeassistant.core", "homeassistant.helpers", "homeassistant.helpers.entity_registry", + "homeassistant.helpers.device_registry", "homeassistant.helpers.selector", # Loaded execution runs beneath the stubs; never escape this file either. "nsh.helpers", @@ -121,6 +122,10 @@ def _install_import_stubs() -> None: sys.modules["homeassistant.helpers.entity_registry"] = entity_registry sys.modules["homeassistant.helpers"] = helpers + device_registry = types.ModuleType("homeassistant.helpers.device_registry") + device_registry.async_get = lambda hass: types.SimpleNamespace(devices={}) + helpers.device_registry = device_registry + sys.modules["homeassistant.helpers.device_registry"] = device_registry selector = types.ModuleType("homeassistant.helpers.selector") selector.EntitySelector = lambda *a, **k: None selector.EntitySelectorConfig = lambda *a, **k: "" diff --git a/tests/test_config_flow_protocol.py b/tests/test_config_flow_protocol.py index a2623fc..91bbe92 100644 --- a/tests/test_config_flow_protocol.py +++ b/tests/test_config_flow_protocol.py @@ -37,6 +37,7 @@ _STUB_NAMES = ( "homeassistant.const", "homeassistant.helpers", "homeassistant.helpers.entity_registry", + "homeassistant.helpers.device_registry", "homeassistant.helpers.selector", "nsh.helpers", "nsh.config_flow", @@ -201,6 +202,10 @@ def _install_stubs() -> None: sys.modules["homeassistant.helpers.entity_registry"] = entity_registry sys.modules["homeassistant.helpers"] = helpers + device_registry = types.ModuleType("homeassistant.helpers.device_registry") + device_registry.async_get = lambda hass: SimpleNamespace(devices={}) + helpers.device_registry = device_registry + sys.modules["homeassistant.helpers.device_registry"] = device_registry selector = types.ModuleType("homeassistant.helpers.selector") selector.EntitySelector = lambda *a, **k: None selector.EntitySelectorConfig = lambda *a, **k: ""