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
@@ -116,6 +116,7 @@ def make_player(modules, manager=None, jellyfin_entity=None, options=None):
|
||||
player._entry = entry
|
||||
player._manager = manager
|
||||
player._intent = None
|
||||
player._intent_timer_unsub = None # mirrors __init__ (intent self-heal)
|
||||
player._last_channel = None
|
||||
player.hass = entity_stublib.FakeHass()
|
||||
player._entity_tasks = set()
|
||||
@@ -123,13 +124,16 @@ def make_player(modules, manager=None, jellyfin_entity=None, options=None):
|
||||
player.async_write_ha_state = lambda: None
|
||||
player._read = lambda eid: None
|
||||
|
||||
# t41: nothing may leave this integration, so an HA service call is now the
|
||||
# failure signal rather than a supported tier. `served` stays empty unless a
|
||||
# regression reintroduces the external-entity delegation.
|
||||
served = []
|
||||
|
||||
async def _cmd(service, **kwargs):
|
||||
served.append(("legacy", service))
|
||||
return None
|
||||
class _RecordingServices:
|
||||
async def async_call(self, domain, service, data=None, **kwargs):
|
||||
served.append((domain, service))
|
||||
|
||||
player._cmd = _cmd
|
||||
player.hass.services = _RecordingServices()
|
||||
|
||||
captured_intents = []
|
||||
|
||||
@@ -248,7 +252,8 @@ class TestChannelDigits:
|
||||
|
||||
await player._channel_entry_sequence(["7"])
|
||||
|
||||
assert served == [("legacy", "play_media")] # transitional path used
|
||||
# t41: both transports down is a no-op — no other integration is called
|
||||
assert served == []
|
||||
|
||||
|
||||
class TestAppAndUrls:
|
||||
@@ -346,38 +351,67 @@ class TestRemoteEntity:
|
||||
assert protocol.key_calls == []
|
||||
|
||||
|
||||
class TestIntentRetryWithoutATransport:
|
||||
"""t41: an intent no transport served surrenders instead of delegating."""
|
||||
|
||||
def _intent(self, **overrides):
|
||||
from types import SimpleNamespace
|
||||
|
||||
intent = SimpleNamespace(
|
||||
target_state="playing",
|
||||
command="media_play",
|
||||
created_at=0.0,
|
||||
prior_state=None,
|
||||
retry_count=0,
|
||||
via_adb=False,
|
||||
via_protocol=False,
|
||||
adb_command=None,
|
||||
protocol_key=None,
|
||||
)
|
||||
for key, value in overrides.items():
|
||||
setattr(intent, key, value)
|
||||
return intent
|
||||
|
||||
async def test_unserved_intent_surrenders_without_a_service_call(self, modules):
|
||||
player, served, _intents = make_player(modules)
|
||||
player.async_write_ha_state = lambda: None
|
||||
player._intent = self._intent()
|
||||
|
||||
player._on_intent_timeout(0.0) # async_call_later passes the timestamp
|
||||
|
||||
assert player._intent is None # surrendered to the truth
|
||||
assert served == [] # and nothing left this integration
|
||||
|
||||
async def test_protocol_served_intent_still_retries_over_the_protocol(self, modules):
|
||||
player, served, _intents = make_player(modules)
|
||||
player.async_write_ha_state = lambda: None
|
||||
protocol = player._manager._protocol
|
||||
player._intent = self._intent(
|
||||
via_protocol=True, protocol_key="KEYCODE_MEDIA_PLAY"
|
||||
)
|
||||
|
||||
player._on_intent_timeout(0.0) # async_call_later passes the timestamp
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
assert protocol.key_calls == [("KEYCODE_MEDIA_PLAY", "SHORT")]
|
||||
assert served == []
|
||||
|
||||
|
||||
class TestAppFallback:
|
||||
async def test_app_id_prefers_the_androidtv_entity(self, modules):
|
||||
"""t41: the app identity comes from OUR transports' store only."""
|
||||
|
||||
async def test_app_id_comes_from_the_manager_store(self, modules):
|
||||
player, _s, _i = make_player(modules)
|
||||
player._androidtv_entity = "media_player.atv"
|
||||
from types import SimpleNamespace as FakeState
|
||||
|
||||
state = FakeState(attributes={"app_id": "com.plex.hdx", "app_name": "Plex"})
|
||||
states = FakeState(states=FakeState(
|
||||
get=lambda eid: state if eid == "media_player.atv" else None
|
||||
))
|
||||
player.hass = states
|
||||
player._read = lambda eid: state if eid == "media_player.atv" else None
|
||||
|
||||
assert player.app_id == "com.plex.hdx"
|
||||
assert player.app_name == "Plex"
|
||||
|
||||
async def test_app_falls_back_to_protocol_current_app(self, modules):
|
||||
player, _s, _i = make_player(modules)
|
||||
player._androidtv_entity = "" # standalone setup (P3-1)
|
||||
player._manager._on_current_app("com.netflix.ninja")
|
||||
player._manager._on_current_app("com.plex.hdx")
|
||||
await asyncio.sleep(0.05) # drain the call_soon_threadsafe push
|
||||
|
||||
assert player.app_id == "com.netflix.ninja"
|
||||
assert player.app_name == "com.netflix.ninja" # package is truth; map later
|
||||
assert player.app_id == "com.plex.hdx"
|
||||
assert player.app_name == "com.plex.hdx" # package is truth; map later
|
||||
|
||||
async def test_app_unavailable_entity_falls_back(self, modules):
|
||||
async def test_app_follows_the_latest_protocol_push(self, modules):
|
||||
player, _s, _i = make_player(modules)
|
||||
player._androidtv_entity = "media_player.atv"
|
||||
from types import SimpleNamespace as FakeState
|
||||
|
||||
player.hass = FakeState(states=FakeState(get=lambda eid: None)) # entity gone
|
||||
player._read = lambda eid: None
|
||||
player._manager._on_current_app("com.netflix.ninja")
|
||||
await asyncio.sleep(0.05)
|
||||
player._manager._on_current_app("com.google.android.youtube.tv")
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
@@ -393,7 +427,6 @@ def _ladder_player(modules, foreground=None, audio=None):
|
||||
manager.store.set("foreground", foreground)
|
||||
if audio:
|
||||
manager.store.set("audio_playback", audio)
|
||||
player._androidtv_entity = ""
|
||||
player._read = lambda eid: None
|
||||
return player, manager
|
||||
|
||||
@@ -411,7 +444,6 @@ class TestForegroundLadderTier:
|
||||
manager.store.set("foreground", foreground)
|
||||
if audio:
|
||||
manager.store.set("audio_playback", audio)
|
||||
player._androidtv_entity = ""
|
||||
player._read = lambda eid: None
|
||||
return player, manager
|
||||
|
||||
@@ -1187,12 +1219,16 @@ def make_manager_with(*, poller=None, protocol=None):
|
||||
)
|
||||
|
||||
|
||||
def make_app_sensor(modules, manager, entry, *, atv_state=None):
|
||||
"""A Current App sensor over the given manager/entry (t40)."""
|
||||
def make_app_sensor(modules, manager, entry):
|
||||
"""A Current App sensor over the given manager/entry (t40).
|
||||
|
||||
t41: the sensor reads the manager's store and nothing else, so the harness
|
||||
hands it a hass whose state machine is empty — a value here can only come
|
||||
from our own transports.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
states = {"media_player.atv": atv_state} if atv_state is not None else {}
|
||||
hass = SimpleNamespace(states=SimpleNamespace(get=states.get))
|
||||
hass = SimpleNamespace(states=SimpleNamespace(get=lambda entity_id: None))
|
||||
return modules["sensor"].NvidiaShieldCurrentAppSensor(hass, entry)
|
||||
|
||||
|
||||
@@ -1232,20 +1268,6 @@ class TestCurrentAppSensorFriendlyName:
|
||||
assert sensor.native_value == "Netflix"
|
||||
assert sensor.extra_state_attributes["app_id"] == "com.netflix.ninja"
|
||||
|
||||
def test_androidtv_entity_tier_serves_when_no_map_or_label(self, modules):
|
||||
from types import SimpleNamespace
|
||||
|
||||
player, _served, _intents = make_player(modules)
|
||||
player._entry.data["androidtv_entity"] = "media_player.atv"
|
||||
atv_state = SimpleNamespace(
|
||||
attributes={"app_id": "ar.tvplayer.tv", "app_name": "ATV Tivi"}
|
||||
)
|
||||
sensor = make_app_sensor(modules, player._manager, player._entry, atv_state=atv_state)
|
||||
|
||||
player._manager._on_current_app("ar.tvplayer.tv")
|
||||
|
||||
assert sensor.native_value == "ATV Tivi"
|
||||
|
||||
def test_recent_apps_fallback_resolves_through_the_label_tier(self, modules):
|
||||
player, _served, _intents = make_player(modules)
|
||||
manager = player._manager
|
||||
@@ -1271,7 +1293,6 @@ class TestCurrentAppSensorFriendlyName:
|
||||
"""One chain, two entities — they cannot disagree about the app name."""
|
||||
player, _served, _intents = make_player(modules)
|
||||
player._entry.options["app_name_map"] = {"ar.tvplayer.tv": "TiviMate"}
|
||||
player._androidtv_entity = "" # standalone, like the unit harness
|
||||
sensor = self._sensor(modules, player)
|
||||
|
||||
player._manager._on_current_app("ar.tvplayer.tv")
|
||||
|
||||
@@ -578,7 +578,7 @@ class TestDiscoveryPrefill:
|
||||
await flow.async_step_zeroconf(_discovery())
|
||||
|
||||
result = await flow.async_step_user(
|
||||
_user_input(**{CONF_ANDROIDTV_ENTITY: "", CONF_ADB_ENABLED: False})
|
||||
_user_input(**{CONF_ADB_ENABLED: False})
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": "no_state_source"}
|
||||
@@ -630,7 +630,6 @@ class TestNoDeadEndPairing:
|
||||
form = await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
@@ -662,7 +661,6 @@ class TestNoDeadEndPairing:
|
||||
form = await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
@@ -695,7 +693,6 @@ class TestNoDeadEndPairing:
|
||||
form = await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
@@ -718,7 +715,6 @@ class TestNoDeadEndPairing:
|
||||
await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
@@ -742,7 +738,6 @@ class TestNoDeadEndPairing:
|
||||
form = await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
@@ -767,7 +762,6 @@ class TestNoDeadEndPairing:
|
||||
form = await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
@@ -787,7 +781,7 @@ class TestNoDeadEndPairing:
|
||||
test_connect = _patch_adb(monkeypatch, module)
|
||||
flow, _hass = _new_flow(module, tmp_path)
|
||||
await flow.async_step_user(
|
||||
_user_input(**{CONF_ANDROIDTV_ENTITY: "", CONF_ADB_ENABLED: True})
|
||||
_user_input(**{CONF_ADB_ENABLED: True})
|
||||
)
|
||||
|
||||
result = await flow.async_step_adb_pair(
|
||||
@@ -806,7 +800,6 @@ class TestNoDeadEndPairing:
|
||||
await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
@@ -835,7 +828,7 @@ class TestNoDeadEndPairing:
|
||||
_patch_adb(monkeypatch, module, connected=False)
|
||||
flow, _hass = _new_flow(module, tmp_path)
|
||||
await flow.async_step_user(
|
||||
_user_input(**{CONF_ANDROIDTV_ENTITY: "", CONF_ADB_ENABLED: True})
|
||||
_user_input(**{CONF_ADB_ENABLED: True})
|
||||
)
|
||||
|
||||
result = await flow.async_step_adb_pair({CONF_ADB_PORT: 5555})
|
||||
@@ -850,7 +843,6 @@ class TestNoDeadEndPairing:
|
||||
await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
|
||||
@@ -289,12 +289,21 @@ def _entry_module():
|
||||
return sys.modules["nsh.entry"]
|
||||
|
||||
|
||||
def _record(sink):
|
||||
"""An async_set_unique_id stand-in that records what the flow tried."""
|
||||
|
||||
async def _set(unique_id):
|
||||
sink.append(unique_id)
|
||||
|
||||
return _set
|
||||
|
||||
|
||||
def _user_input(**overrides) -> dict:
|
||||
base = {
|
||||
"name": "mdarcy",
|
||||
CONF_ANDROIDTV_ENTITY: "media_player.shield",
|
||||
CONF_PROTOCOL_ENABLED: False,
|
||||
CONF_PROTOCOL_HOST: "",
|
||||
CONF_ADB_ENABLED: False,
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
@@ -306,21 +315,25 @@ class TestStandaloneValidation:
|
||||
flow = module.NvidiaShieldConfigFlow()
|
||||
flow.hass = FakeHass(str(tmp_path))
|
||||
|
||||
result = await flow.async_step_user(_user_input(**{CONF_ANDROIDTV_ENTITY: ""}))
|
||||
result = await flow.async_step_user(_user_input())
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "no_state_source"}
|
||||
|
||||
async def test_androidtv_entity_alone_is_a_valid_source(self, tmp_path):
|
||||
async def test_the_androidtv_field_is_gone_from_the_form(self, tmp_path):
|
||||
"""t41: we own the stack — no other integration is a state source."""
|
||||
module = _form_module()
|
||||
flow = module.NvidiaShieldConfigFlow()
|
||||
flow.hass = FakeHass(str(tmp_path))
|
||||
|
||||
result = await flow.async_step_user(_user_input())
|
||||
result = await flow.async_step_user(None)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"] == {CONF_ANDROIDTV_ENTITY: "media_player.shield"}
|
||||
fields = vol_mirror.declared_fields(result["data_schema"])
|
||||
assert CONF_ANDROIDTV_ENTITY not in fields
|
||||
# the two native transports are what is left to choose from
|
||||
assert CONF_PROTOCOL_ENABLED in fields
|
||||
assert CONF_ADB_ENABLED in fields
|
||||
|
||||
async def test_standalone_protocol_goes_to_pairing(self, tmp_path, monkeypatch):
|
||||
module = _form_module()
|
||||
@@ -345,20 +358,19 @@ class TestStandaloneValidation:
|
||||
)
|
||||
|
||||
result = await flow.async_step_user(
|
||||
_user_input(**{CONF_ANDROIDTV_ENTITY: "", CONF_PROTOCOL_ENABLED: True})
|
||||
_user_input(**{CONF_PROTOCOL_ENABLED: True})
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "protocol_pair" # routed into pairing, no error
|
||||
|
||||
async def test_standalone_entry_data_omits_the_field(self, tmp_path, monkeypatch):
|
||||
"""No androidtv entity → the key is absent from entry data (not "")."""
|
||||
async def test_entry_data_is_empty_so_options_own_the_config(self, tmp_path, monkeypatch):
|
||||
"""t41: every value lives in options; `data` carries no leftovers."""
|
||||
module = _form_module()
|
||||
flow = module.NvidiaShieldConfigFlow()
|
||||
flow.hass = FakeHass(str(tmp_path))
|
||||
flow._user_data = {
|
||||
"name": "mdarcy",
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: "shield.local",
|
||||
}
|
||||
@@ -367,7 +379,7 @@ class TestStandaloneValidation:
|
||||
result = flow._create_entry()
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert CONF_ANDROIDTV_ENTITY not in result["data"]
|
||||
assert result["data"] == {}
|
||||
|
||||
|
||||
class TestAdbOnlyStandalone:
|
||||
@@ -380,7 +392,6 @@ class TestAdbOnlyStandalone:
|
||||
result = await flow.async_step_user(
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ANDROIDTV_ENTITY: "",
|
||||
CONF_PROTOCOL_ENABLED: False,
|
||||
"adb_enabled": True,
|
||||
}
|
||||
@@ -398,7 +409,7 @@ class TestAdbOnlyStandalone:
|
||||
flow.hass = FakeHass(str(tmp_path))
|
||||
|
||||
result = await flow.async_step_user(
|
||||
_user_input(**{CONF_ANDROIDTV_ENTITY: "", "adb_enabled": False})
|
||||
_user_input(**{"adb_enabled": False})
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": "no_state_source"}
|
||||
@@ -567,69 +578,33 @@ class TestChannelEntryOptions:
|
||||
assert result["errors"] == {"base": "invalid_digit_delay"}
|
||||
|
||||
|
||||
class TestUniqueIdSourceG2:
|
||||
"""t29 G2: no unique_id equals the androidtv entity_id anymore."""
|
||||
class TestUniqueIdSource:
|
||||
"""t41: no unique_id is derived from another integration, at any stage.
|
||||
|
||||
def test_entity_unique_id_fallback_is_gone(self):
|
||||
from pathlib import Path
|
||||
t29 G2 replaced the renameable entity_id with a device-MAC derive; t41
|
||||
removes the entity path entirely, so the ONLY identity source left is the
|
||||
protocol MAC learned during pairing (``async_set_unique_id(mac)``).
|
||||
"""
|
||||
|
||||
base = Path(__file__).parent.parent / "custom_components" / "nvidia_shield"
|
||||
flow_src = (base / "config_flow.py").read_text()
|
||||
# the historic renameable fallback is gone from the user step
|
||||
def test_no_entity_derived_unique_id_remains(self):
|
||||
flow_src = (_SHIELD_DIR / "config_flow.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "_unique_id_from_atv_device" not in flow_src
|
||||
assert "async_set_unique_id(androidtv_entity)" not in flow_src
|
||||
# the documented replacement: a device-MAC derive, else no unique_id
|
||||
assert "_unique_id_from_atv_device" in flow_src
|
||||
|
||||
def test_entity_rename_never_moves_the_unique_id(self):
|
||||
"""The MAC-derived id is rename-proof; a MAC-less entity has none."""
|
||||
|
||||
# no registry lookups for another integration's devices either
|
||||
assert "device_registry" not in flow_src
|
||||
|
||||
async def test_the_user_step_sets_no_unique_id(self, tmp_path):
|
||||
module = _form_module()
|
||||
# a registered atv entity whose device carries a MAC
|
||||
entity = SimpleNamespace(domain="media_player", device_id="dev1", platform="lgtv")
|
||||
device = SimpleNamespace(
|
||||
connections=[("mac", None), ("mac", "aa:bb:cc:dd:ee:ff")]
|
||||
)
|
||||
er_stub = module.er
|
||||
er_stub.async_get = lambda hass: SimpleNamespace(
|
||||
entities={"media_player.shield": entity}
|
||||
)
|
||||
class _DevRegistry:
|
||||
def __init__(self, by_id):
|
||||
self._by_id = by_id
|
||||
flow = module.NvidiaShieldConfigFlow()
|
||||
flow.hass = FakeHass(str(tmp_path))
|
||||
flow.async_show_form = lambda **kwargs: {"type": "form", **kwargs}
|
||||
recorded = []
|
||||
flow.async_set_unique_id = _record(recorded)
|
||||
|
||||
def async_get(self, device_id):
|
||||
return self._by_id.get(device_id)
|
||||
await flow.async_step_user(_user_input(**{CONF_PROTOCOL_ENABLED: True}))
|
||||
|
||||
module.dr.async_get = lambda hass: _DevRegistry({"dev1": device})
|
||||
|
||||
# the resolved unique_id is the MAC — not the entity_id
|
||||
uid = module._unique_id_from_atv_device(
|
||||
FakeHass("/tmp"), "media_player.shield"
|
||||
)
|
||||
|
||||
assert uid == "aa:bb:cc:dd:ee:ff"
|
||||
|
||||
# renaming the entity does not affect the resolved value
|
||||
er_stub.async_get = lambda hass: SimpleNamespace(
|
||||
entities={"media_player.renamed": entity}
|
||||
)
|
||||
uid_renamed = module._unique_id_from_atv_device(
|
||||
FakeHass("/tmp"), "media_player.renamed"
|
||||
)
|
||||
|
||||
assert uid_renamed == "aa:bb:cc:dd:ee:ff" # stable across renames
|
||||
|
||||
def test_macless_entity_yields_no_unique_id(self):
|
||||
|
||||
|
||||
module = _form_module()
|
||||
er_stub = module.er
|
||||
er_stub.async_get = lambda hass: SimpleNamespace(entities={}) # unregistered
|
||||
assert (
|
||||
module._unique_id_from_atv_device(FakeHass("/tmp"), "media_player.x")
|
||||
is None
|
||||
)
|
||||
assert recorded == []
|
||||
|
||||
|
||||
class TestListenerComboGone:
|
||||
@@ -1220,7 +1195,6 @@ class TestAppNameMapResolution:
|
||||
modules = entity_stublib.load_entity_modules()
|
||||
player, _s, _i = _cmd_player(modules)
|
||||
player._entry.options["app_name_map"] = {"ar.tvplayer.tv": "TiviMate"}
|
||||
player._androidtv_entity = "" # standalone
|
||||
|
||||
manager = player._manager
|
||||
manager._on_current_app("ar.tvplayer.tv") # protocol push
|
||||
@@ -1235,7 +1209,6 @@ class TestAppNameMapResolution:
|
||||
modules = entity_stublib.load_entity_modules()
|
||||
player, _s, _i = _cmd_player(modules)
|
||||
player._entry.options["app_name_map"] = {"ar.tvplayer.tv": "TiviMate"}
|
||||
player._androidtv_entity = ""
|
||||
|
||||
manager = player._manager
|
||||
manager._on_current_app("com.google.android.youtube.tv")
|
||||
@@ -1243,23 +1216,17 @@ class TestAppNameMapResolution:
|
||||
|
||||
assert player.app_name == "com.google.android.youtube.tv"
|
||||
|
||||
async def test_entity_tier_serves_when_no_higher_tier_matches(self):
|
||||
"""P5-1: the entity tier is below map/manifest — it serves when the
|
||||
package has no explicit override and no manifest label yet."""
|
||||
from types import SimpleNamespace as SimpleNS
|
||||
|
||||
async def test_cached_manifest_label_is_the_second_tier(self):
|
||||
"""P5-1: below the explicit map sits the device manifest label."""
|
||||
import entity_stublib
|
||||
|
||||
modules = entity_stublib.load_entity_modules()
|
||||
player, _s, _i = _cmd_player(modules)
|
||||
player._entry.options["app_name_map"] = {} # no override
|
||||
player._androidtv_entity = "media_player.atv"
|
||||
state = SimpleNS(attributes={"app_id": "ar.tvplayer.tv", "app_name": "ATV Tivi"})
|
||||
player.hass = SimpleNS(states=SimpleNS(get=lambda eid: state))
|
||||
player._read = lambda eid: state if eid == "media_player.atv" else None
|
||||
manager = player._manager
|
||||
manager._app_labels["ar.tvplayer.tv"] = ("TiviMate", manager._clock())
|
||||
|
||||
player._manager._on_current_app("ar.tvplayer.tv")
|
||||
manager._on_current_app("ar.tvplayer.tv")
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
# no cached label yet → falls through to the entity tier
|
||||
assert player.app_name == "ATV Tivi"
|
||||
assert player.app_name == "TiviMate"
|
||||
Reference in new issue
Block a user