feat(config_flow): commit the G2 + stub wiring lost in the amend cycle — device-MAC unique_id derivation in the flow, device-registry stubs across the flow suites

This commit is contained in:
James Mackay committed 2026-10-09 07:11:11 +01:00
1 parent e1a0fc3039
commit 383ec5ada5
4 files changed
+69 -2

No files matched your search

+41 -2
View File
@@ -6,6 +6,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.core import callback 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 entity_registry as er
from homeassistant.helpers import selector 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 [] 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: def _find_jellyfin_entity(hass, device_name: str) -> str | None:
"""Auto-detect the Jellyfin media_player entity for a device.""" """Auto-detect the Jellyfin media_player entity for a device."""
registry = er.async_get(hass) registry = er.async_get(hass)
@@ -214,8 +243,18 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
if androidtv_entity: # t29 G2: the androidtv entity_id is a renameable reference —
await self.async_set_unique_id(androidtv_entity) # 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() self._abort_if_unique_id_configured()
# Auto-detect Jellyfin if not manually specified # Auto-detect Jellyfin if not manually specified
+18
View File
@@ -24,6 +24,7 @@ ENTITIES_STUB_NAMES = (
"homeassistant.components.button", "homeassistant.components.button",
"homeassistant.helpers", "homeassistant.helpers",
"homeassistant.helpers.entity_registry", "homeassistant.helpers.entity_registry",
"homeassistant.helpers.device_registry",
"homeassistant.helpers.selector", "homeassistant.helpers.selector",
"homeassistant.helpers.event", "homeassistant.helpers.event",
"homeassistant.helpers.entity", "homeassistant.helpers.entity",
@@ -253,9 +254,26 @@ def install() -> None:
return None return None
entity_registry.async_get = lambda hass: _Registry() 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 helpers.entity_registry = entity_registry
sys.modules["homeassistant.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 = types.ModuleType("homeassistant.helpers.event")
event.async_call_later = lambda hass, delay, callback_fn: (lambda: None) event.async_call_later = lambda hass, delay, callback_fn: (lambda: None)
event.async_track_state_change_event = lambda *args, **kwargs: (lambda: None) event.async_track_state_change_event = lambda *args, **kwargs: (lambda: None)
+5
View File
@@ -35,6 +35,7 @@ _STUB_NAMES = (
"homeassistant.core", "homeassistant.core",
"homeassistant.helpers", "homeassistant.helpers",
"homeassistant.helpers.entity_registry", "homeassistant.helpers.entity_registry",
"homeassistant.helpers.device_registry",
"homeassistant.helpers.selector", "homeassistant.helpers.selector",
# Loaded execution runs beneath the stubs; never escape this file either. # Loaded execution runs beneath the stubs; never escape this file either.
"nsh.helpers", "nsh.helpers",
@@ -121,6 +122,10 @@ def _install_import_stubs() -> None:
sys.modules["homeassistant.helpers.entity_registry"] = entity_registry sys.modules["homeassistant.helpers.entity_registry"] = entity_registry
sys.modules["homeassistant.helpers"] = helpers 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 = types.ModuleType("homeassistant.helpers.selector")
selector.EntitySelector = lambda *a, **k: None selector.EntitySelector = lambda *a, **k: None
selector.EntitySelectorConfig = lambda *a, **k: "" selector.EntitySelectorConfig = lambda *a, **k: ""
+5
View File
@@ -37,6 +37,7 @@ _STUB_NAMES = (
"homeassistant.const", "homeassistant.const",
"homeassistant.helpers", "homeassistant.helpers",
"homeassistant.helpers.entity_registry", "homeassistant.helpers.entity_registry",
"homeassistant.helpers.device_registry",
"homeassistant.helpers.selector", "homeassistant.helpers.selector",
"nsh.helpers", "nsh.helpers",
"nsh.config_flow", "nsh.config_flow",
@@ -201,6 +202,10 @@ def _install_stubs() -> None:
sys.modules["homeassistant.helpers.entity_registry"] = entity_registry sys.modules["homeassistant.helpers.entity_registry"] = entity_registry
sys.modules["homeassistant.helpers"] = helpers 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 = types.ModuleType("homeassistant.helpers.selector")
selector.EntitySelector = lambda *a, **k: None selector.EntitySelector = lambda *a, **k: None
selector.EntitySelectorConfig = lambda *a, **k: "" selector.EntitySelectorConfig = lambda *a, **k: ""