fix(poller): F1 — the poller factory re-reads entry.options at call time (audit-sanctioned option), so the next built poller targets the NEW address after a zeroconf IP change; the stale live-read comment in config_flow points at the actual mechanism; TestIpChangeSelfHealF1 drives an IP change and pins the next poller host (t32)
This commit is contained in:
1 parent
c1a82a2fa5
commit
8f40b6435c
3 files changed
+91
-2
No files matched your search
@@ -130,7 +130,18 @@ async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return lambda: AdbMediaSessionPoller(adb.host, adb.port, adb.key_path)
|
# t32 F1 (review repair): the audit-sanctioned FACTORY RE-READ — the
|
||||||
|
# host/port resolve from entry.options AT CALL TIME, so the poller built
|
||||||
|
# after a zeroconf IP change targets the NEW address without a reload
|
||||||
|
# (the setup-time closure pinned the old address). A key-path change
|
||||||
|
# still needs the Pair ADB button / reconfigure reload, by design.
|
||||||
|
def _factory():
|
||||||
|
adb_now = resolve_adb_config(hass, entry)
|
||||||
|
if adb_now is None:
|
||||||
|
return None
|
||||||
|
return AdbMediaSessionPoller(adb_now.host, adb_now.port, adb_now.key_path)
|
||||||
|
|
||||||
|
return _factory
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|||||||
@@ -363,7 +363,8 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if options != entry.options:
|
if options != entry.options:
|
||||||
# t29 G3a: the deprecated update+schedule-reload combo is
|
# t29 G3a: the deprecated update+schedule-reload combo is
|
||||||
# gone (2026.12 error) — the entry updates ONLY here; the
|
# gone (2026.12 error) — the entry updates ONLY here; the
|
||||||
# manager reads the host options live on the next cycle.
|
# self-heal rides on the t32 F1 poller factory, which
|
||||||
|
# re-reads entry.options at call time (no reload).
|
||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
entry, options=options
|
entry, options=options
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ migration validity for v2 entries without the field.
|
|||||||
import asyncio # noqa: F401 (async steps need the event loop)
|
import asyncio # noqa: F401 (async steps need the event loop)
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
from pathlib import Path # noqa: E402 (F1 test key staging)
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from conftest import _SHIELD_DIR, _load_module
|
from conftest import _SHIELD_DIR, _load_module
|
||||||
from nsh.const import (
|
from nsh.const import (
|
||||||
|
CONF_ADB_ENABLED,
|
||||||
|
CONF_ADB_HOST,
|
||||||
|
CONF_ADB_PORT,
|
||||||
CONF_ANDROIDTV_ENTITY,
|
CONF_ANDROIDTV_ENTITY,
|
||||||
CONF_PROTOCOL_ENABLED,
|
CONF_PROTOCOL_ENABLED,
|
||||||
CONF_PROTOCOL_HOST,
|
CONF_PROTOCOL_HOST,
|
||||||
@@ -758,6 +763,78 @@ class TestLegacyUniqueIdMigration:
|
|||||||
assert hass.config_entries.updating_entry_calls == [] # untouched
|
assert hass.config_entries.updating_entry_calls == [] # untouched
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpChangeSelfHealF1:
|
||||||
|
"""t32 F1 (HIGH): the poller factory re-reads entry.options at call
|
||||||
|
time, so the NEXT built poller targets the NEW address after a
|
||||||
|
zeroconf IP change (the audit-sanctioned factory re-read option)."""
|
||||||
|
|
||||||
|
def _adb_entry(self, host="192.168.1.50"):
|
||||||
|
return SimpleNamespace(
|
||||||
|
entry_id="e1",
|
||||||
|
title="Shield",
|
||||||
|
data={CONF_ANDROIDTV_ENTITY: "media_player.shield"},
|
||||||
|
options={
|
||||||
|
CONF_ADB_ENABLED: True,
|
||||||
|
CONF_ADB_HOST: host,
|
||||||
|
CONF_ADB_PORT: 5555,
|
||||||
|
},
|
||||||
|
runtime_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _hass_with_key(self, tmp_path):
|
||||||
|
import conftest
|
||||||
|
import entity_stublib
|
||||||
|
|
||||||
|
entity_stublib.install()
|
||||||
|
if "nsh.init" not in sys.modules:
|
||||||
|
conftest._load_module("nsh.init", conftest._SHIELD_DIR / "__init__.py")
|
||||||
|
init_module = sys.modules["nsh.init"]
|
||||||
|
hass = FakeHass(str(tmp_path))
|
||||||
|
key_dir = Path(hass.config.config_dir) / ".storage" / "nvidia_shield"
|
||||||
|
key_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(key_dir / "adbkey").write_text("test-key")
|
||||||
|
return init_module, hass
|
||||||
|
|
||||||
|
async def test_built_poller_targets_the_new_address(self, tmp_path):
|
||||||
|
"""The delivered self-heal: after the zeroconf host update the
|
||||||
|
NEXT built poller targets the new address (no reload involved)."""
|
||||||
|
init_module, hass = self._hass_with_key(tmp_path)
|
||||||
|
entry = self._adb_entry()
|
||||||
|
hass.config_entries.entries = [entry]
|
||||||
|
|
||||||
|
factory = await init_module._build_poller_factory(hass, entry)
|
||||||
|
assert factory is not None
|
||||||
|
|
||||||
|
first = factory()
|
||||||
|
assert first is not None
|
||||||
|
# AdbClientBase exposes the connection config as a dict (t27 F-NEW3)
|
||||||
|
assert first.config["host"] == "192.168.1.50"
|
||||||
|
|
||||||
|
# the config-flow zeroconf path updates the entry's hosts via
|
||||||
|
# async_update_entry — the NEXT built poller targets the NEW address
|
||||||
|
entry.options = {
|
||||||
|
**entry.options,
|
||||||
|
CONF_ADB_HOST: "192.168.1.77",
|
||||||
|
CONF_PROTOCOL_HOST: "192.168.1.77",
|
||||||
|
}
|
||||||
|
|
||||||
|
second = factory()
|
||||||
|
assert second is not None
|
||||||
|
assert second.config["host"] == "192.168.1.77"
|
||||||
|
|
||||||
|
async def test_unresolvable_call_stays_passive(self, tmp_path):
|
||||||
|
"""A momentary unresolvable host yields None (passive cycle) — the
|
||||||
|
watchdog's existing None handling stays intact."""
|
||||||
|
init_module, hass = self._hass_with_key(tmp_path)
|
||||||
|
entry = self._adb_entry(host="10.255.255.1")
|
||||||
|
|
||||||
|
factory = await init_module._build_poller_factory(hass, entry)
|
||||||
|
assert factory is not None
|
||||||
|
|
||||||
|
with mock.patch("nsh.init.resolve_adb_config", return_value=None):
|
||||||
|
assert factory() is None
|
||||||
|
|
||||||
|
|
||||||
class TestZeroconfDiscovery:
|
class TestZeroconfDiscovery:
|
||||||
"""P4-1a: _androidtvremote2._tcp discovery → pre-filled user step."""
|
"""P4-1a: _androidtvremote2._tcp discovery → pre-filled user step."""
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user