refactor(config): drop the androidtv media_player entity path end-to-end (t41)
The user ruling: this integration REPLACES both old integrations, so nothing may depend on an androidtv/androidtv_remote entity anymore. Removed: * the setup-form field + its entity selector (CONF_ANDROIDTV_ENTITY is no longer offered, only read by the migration that drops it); * the entity-derived unique_id derivation (_unique_id_from_atv_device, t29 G2) and, with it, every registry lookup for another integration's devices — the user step now sets NO unique_id at all; the protocol MAC learned during pairing is the identity; * the Jellyfin auto-detect's second tier (the atv entity's friendly_name) — it now uses the entry name the user typed or the device name from discovery; * the app-name and app-id tiers (media_player.app_name/app_id + the shared resolve_app_name chain, the Current App sensor, the sensor's state-change listener and its `_androidtv_state` helper) — the manager's store is the only source; * the "transitional external entity" command tier in every path: `_cmd` and the pan-media_player service call are gone, and so is the fallback in _media_cmd, _remote_key_cmd, the §7 channel-digit sequence and the intent self-heal retry (an intent no transport served now surrenders to the truth instead of delegating); * the entity-derived power signal (`_on_source_change`'s "device off clears the intent" check no longer watches another integration's entity; the power LADDER — protocol is_on → ADB wake → power switch — is unchanged). turn_on/turn_off are native now: they send the protocol POWER key and, like the remote entity, send nothing when the known state already matches (no superfluous toggle). An ADB-only entry (protocol skipped) has no power write path — the manager owns the socket and exposes no device-keyevent, which is what the old delegation used to cover. helpers.discover_adb_host — the one helper that derived a host from another integration's config entry — is deleted; button.py (granted to this task) reads the entry's own address fields instead of the entity, so the Pair ADB button keeps working. Present-day entries that still carry the key are unaffected (nothing reads it); the v3 migration that drops it lands with the host unification. Verified: 309 passed / 2 skipped, 310 / 1 with androidtvremote2, ruff clean, drift engine exit 0 on HEAD (13 modules, 58 imports, 35 call arguments, 6 awaited HA calls) and 16 passed in the real-HA drift suite. The stub harness's legacy service recorder became a watchdog: any HA service call from the media player now fails the routing tests instead of being the expected outcome.
This commit is contained in:
1 parent
60d884b76a
commit
86aa2827a2
9 files changed
+247
-351
No files matched your search
@@ -6,7 +6,6 @@ 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
|
||||
|
||||
@@ -19,7 +18,6 @@ from .const import (
|
||||
CONF_ADB_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ADB_STORE_APP_INFO,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_APP_ICON_MAP,
|
||||
CONF_APP_NAME_MAP,
|
||||
CONF_CHANNEL_DIGIT_DELAY,
|
||||
@@ -37,7 +35,7 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from .helpers import discover_adb_host, protocol_cert_paths
|
||||
from .helpers import protocol_cert_paths
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,34 +51,6 @@ 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)
|
||||
@@ -169,7 +139,7 @@ def _host_field_key(field: str, host: str):
|
||||
|
||||
|
||||
def _user_schema_dict(sel, defaults: dict | None = None) -> dict:
|
||||
"""User-step schema — the androidtv entity is optional (standalone).
|
||||
"""User-step schema — every field belongs to THIS integration (t41).
|
||||
|
||||
``defaults`` pre-fills name/hosts from a zeroconf discovery event (or a
|
||||
YAML seed hint); the ADB port is pre-filled with the standard 5555 so it
|
||||
@@ -182,7 +152,6 @@ def _user_schema_dict(sel, defaults: dict | None = None) -> dict:
|
||||
port = DEFAULT_ADB_PORT
|
||||
schema_dict = {
|
||||
vol.Required("name", default=defaults.get("name", DEFAULT_NAME)): str,
|
||||
vol.Optional(CONF_ANDROIDTV_ENTITY): sel["androidtv"],
|
||||
vol.Optional(CONF_JELLYFIN_ENTITY): sel["media_player"],
|
||||
vol.Optional(CONF_RECEIVER_ENTITY): sel["receiver"],
|
||||
}
|
||||
@@ -207,9 +176,6 @@ def _user_schema_dict(sel, defaults: dict | None = None) -> dict:
|
||||
def _build_selectors():
|
||||
"""Build reusable selector instances."""
|
||||
return {
|
||||
"androidtv": selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain="media_player", device_class="tv")
|
||||
),
|
||||
"media_player": selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain="media_player")
|
||||
),
|
||||
@@ -243,51 +209,32 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Step 1: Basic setup.
|
||||
|
||||
Standalone setup (Phase 3): any one of the three state sources is
|
||||
enough — the Android TV Remote entity, the Nvidia Remote Protocol
|
||||
(pairing), or ADB media-session mode (CONF_ADB_ENABLED). The
|
||||
``no_state_source`` error fires only when none of them is present.
|
||||
We own the whole stack (t41), so the two state sources are ours: the
|
||||
Nvidia Remote Protocol (pairing) and ADB media-session mode
|
||||
(CONF_ADB_ENABLED). The ``no_state_source`` error fires only when
|
||||
neither is enabled.
|
||||
"""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
androidtv_entity = user_input.get(CONF_ANDROIDTV_ENTITY)
|
||||
|
||||
has_source = (
|
||||
bool(androidtv_entity)
|
||||
or bool(user_input.get(CONF_PROTOCOL_ENABLED))
|
||||
or bool(user_input.get(CONF_ADB_ENABLED))
|
||||
has_source = bool(user_input.get(CONF_PROTOCOL_ENABLED)) or bool(
|
||||
user_input.get(CONF_ADB_ENABLED)
|
||||
)
|
||||
if not has_source:
|
||||
errors["base"] = "no_state_source"
|
||||
return self._show_user_form(errors)
|
||||
|
||||
# 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()
|
||||
# t41: nothing here derives a unique_id from another integration.
|
||||
# The entry's identity is the protocol MAC learned during pairing
|
||||
# (`async_set_unique_id(mac)` in the pairing step) or the config
|
||||
# entry itself — never a renameable entity id (t29 G2's
|
||||
# atv-device derivation went with the entity path).
|
||||
|
||||
# Auto-detect Jellyfin if not manually specified
|
||||
# Auto-detect Jellyfin from the names setup already knows: the
|
||||
# entry name the user typed, or the device name from discovery.
|
||||
jellyfin = user_input.get(CONF_JELLYFIN_ENTITY)
|
||||
if not jellyfin:
|
||||
device_name = user_input.get("name", DEFAULT_NAME)
|
||||
atv_state = (
|
||||
self.hass.states.get(androidtv_entity)
|
||||
if androidtv_entity
|
||||
else None
|
||||
)
|
||||
atv_name = (atv_state.attributes.get("friendly_name", "") if atv_state else "")
|
||||
|
||||
for name in [device_name, atv_name]:
|
||||
for name in (user_input.get("name", DEFAULT_NAME), self._discovered_name):
|
||||
if not name:
|
||||
continue
|
||||
found = _find_jellyfin_entity(self.hass, name)
|
||||
@@ -298,7 +245,6 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._user_data = {
|
||||
"name": user_input.get("name", DEFAULT_NAME),
|
||||
CONF_ANDROIDTV_ENTITY: androidtv_entity or "",
|
||||
CONF_JELLYFIN_ENTITY: jellyfin or "",
|
||||
CONF_RECEIVER_ENTITY: user_input.get(CONF_RECEIVER_ENTITY, ""),
|
||||
CONF_RECEIVER_INPUT: user_input.get(CONF_RECEIVER_INPUT, ""),
|
||||
@@ -433,14 +379,16 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_user()
|
||||
|
||||
def _create_entry(self):
|
||||
"""Create the config entry from stored user data."""
|
||||
"""Create the config entry from stored user data.
|
||||
|
||||
Everything the integration needs lives in the OPTIONS (t41: one host
|
||||
field among them); ``data`` stays empty because no value has to be
|
||||
immutable.
|
||||
"""
|
||||
d = self._user_data
|
||||
entry_data = {}
|
||||
if d.get(CONF_ANDROIDTV_ENTITY):
|
||||
entry_data[CONF_ANDROIDTV_ENTITY] = d[CONF_ANDROIDTV_ENTITY]
|
||||
return self.async_create_entry(
|
||||
title=d["name"],
|
||||
data=entry_data,
|
||||
data={},
|
||||
options={
|
||||
CONF_JELLYFIN_ENTITY: d.get(CONF_JELLYFIN_ENTITY, ""),
|
||||
CONF_RECEIVER_ENTITY: d.get(CONF_RECEIVER_ENTITY, ""),
|
||||
@@ -722,7 +670,6 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# the entry already knows (and _create_entry is never reached here).
|
||||
self._user_data = {
|
||||
"name": entry.title,
|
||||
CONF_ANDROIDTV_ENTITY: entry.data.get(CONF_ANDROIDTV_ENTITY, ""),
|
||||
CONF_ADB_ENABLED: entry.options.get(CONF_ADB_ENABLED, False),
|
||||
CONF_ADB_HOST: entry.options.get(CONF_ADB_HOST, ""),
|
||||
CONF_ADB_PORT: entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
|
||||
@@ -774,9 +721,11 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
def _resolve_adb_host(self) -> str | None:
|
||||
"""Resolve ADB host from explicit config or the Android TV entity."""
|
||||
return self._user_data.get(CONF_ADB_HOST) or discover_adb_host(
|
||||
self.hass, self._user_data.get(CONF_ANDROIDTV_ENTITY)
|
||||
"""The address to use for ADB: the ADB field, else the protocol field."""
|
||||
return (
|
||||
self._user_data.get(CONF_ADB_HOST)
|
||||
or self._user_data.get(CONF_PROTOCOL_HOST)
|
||||
or None
|
||||
)
|
||||
|
||||
def _show_adb_pair_form(self, errors=None):
|
||||
|
||||
Reference in new issue
Block a user