refactor(config): one Shield IP field for both transports (t41)
The two address fields were the same Shield spelled twice, and every bug in the zeroconf/IP-change path came from keeping them in sync. One field now: * const.CONF_HOST replaces CONF_ADB_HOST / CONF_PROTOCOL_HOST in the setup form, the options page, the pairing forms, the reconfigure/reauth seeding and the description copy. ADB port 5555 stays the only port field; * helpers.entry_host(entry) is the single resolver (options first, then data); resolve_adb_config and the protocol settings both go through it, so both transports read the same value at call time (the t32 F1 factory re-read keeps working: the NEXT built poller sees the new address); * helpers.seed_host / seed_adb_enabled own the YAML seed's shape rule. A block may use `host` or the legacy protocol_host/adb_host keys — t41 keeps the old spelling working — and t32 F4's outcome is unchanged: a protocol-only block leaves ADB off, an address (or an explicit adb_enabled) turns it on; * the zeroconf IP-change path collapses the t21-F2 per-host dance into one rule: the discovery address IS the entry's address, so the entry is updated when it differs and left alone when it matches. The blank-self-heal and deliberately-different-ADB-host cases are gone with the duplication — a second address field cannot express them anymore, and the tests now pin that (previously "an explicit override is never clobbered"); * the options page exposes the protocol toggle it never had (the page could turn ADB off but not the protocol); * button.py (granted to this task) reads the entry's own address instead of another integration's entity. The v3 entry migration that maps existing adb_host/protocol_host onto CONF_HOST and drops the androidtv key lands in the next commit; HA runs migrations before setup, so an existing entry is never read with the old shape. Verified: 311 passed / 2 skipped, 312 / 1 with androidtvremote2, ruff clean, drift engine exit 0 on HEAD. The UI-copy contract tests (every rendered field has a label in strings.json AND translations/en.json, key trees identical, host errors name the field) drove the string edits.
This commit is contained in:
1 parent
86aa2827a2
commit
75ebeb40b0
12 files changed
+302
-276
No files matched your search
@@ -15,10 +15,9 @@ import pytest
|
||||
from conftest import _SHIELD_DIR, _load_module
|
||||
from nsh.const import (
|
||||
CONF_ADB_ENABLED,
|
||||
CONF_ADB_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ADB_STORE_APP_INFO,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_HOST,
|
||||
CONF_JELLYFIN_ENTITY,
|
||||
CONF_POWER_ENTITY,
|
||||
CONF_RECEIVER_ENTITY,
|
||||
@@ -164,9 +163,8 @@ def _flow(user_input_host: str, monkeypatch=None):
|
||||
flow._user_data = {
|
||||
"name": "Nvidia Shield",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: user_input_host,
|
||||
CONF_HOST: user_input_host,
|
||||
CONF_ADB_PORT: 5555,
|
||||
CONF_ANDROIDTV_ENTITY: "media_player.shield",
|
||||
CONF_JELLYFIN_ENTITY: "",
|
||||
CONF_RECEIVER_ENTITY: "",
|
||||
CONF_RECEIVER_INPUT: "",
|
||||
|
||||
@@ -15,15 +15,13 @@ import pytest
|
||||
from conftest import _SHIELD_DIR, _load_module
|
||||
from nsh.const import (
|
||||
CONF_ADB_ENABLED,
|
||||
CONF_ADB_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ADB_STORE_APP_INFO,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_HOST,
|
||||
CONF_JELLYFIN_ENTITY,
|
||||
CONF_PAIRING_CODE,
|
||||
CONF_POWER_ENTITY,
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
CONF_PROTOCOL_HOST,
|
||||
CONF_RECEIVER_ENTITY,
|
||||
CONF_RECEIVER_INPUT,
|
||||
DEFAULT_ADB_PORT,
|
||||
@@ -296,17 +294,15 @@ def make_flow(tmp_dir: str, monkeypatch: pytest.MonkeyPatch, client: FakeProtoco
|
||||
flow.hass = FakeHass(tmp_dir)
|
||||
flow._user_data = {
|
||||
"name": "mdarcy",
|
||||
CONF_ANDROIDTV_ENTITY: "media_player.shield",
|
||||
CONF_JELLYFIN_ENTITY: "",
|
||||
CONF_RECEIVER_ENTITY: "",
|
||||
CONF_RECEIVER_INPUT: "",
|
||||
CONF_POWER_ENTITY: "",
|
||||
CONF_ADB_ENABLED: False,
|
||||
CONF_ADB_STORE_APP_INFO: True,
|
||||
CONF_ADB_HOST: "",
|
||||
CONF_HOST: "shield.local", # t41: ONE address field
|
||||
CONF_ADB_PORT: DEFAULT_ADB_PORT,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: "shield.local",
|
||||
}
|
||||
from nsh.connection import ShieldConnectionManager
|
||||
|
||||
@@ -350,7 +346,7 @@ class TestProtocolPairing:
|
||||
assert client.disconnected == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["options"][CONF_PROTOCOL_ENABLED] is True
|
||||
assert result["options"][CONF_PROTOCOL_HOST] == "shield.local"
|
||||
assert result["options"][CONF_HOST] == "shield.local"
|
||||
|
||||
async def test_wrong_code_asks_again_then_succeeds(self, tmp_path, monkeypatch):
|
||||
client = FakeProtocolClient()
|
||||
@@ -374,7 +370,7 @@ class TestReauthFlow:
|
||||
flow = make_flow(str(tmp_path), monkeypatch, client)
|
||||
entry = SimpleNamespace(
|
||||
entry_id="e1", title="mdarcy",
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_PROTOCOL_HOST: "shield.local"},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: "shield.local"},
|
||||
data={},
|
||||
)
|
||||
flow.hass.config_entries.__dict__["__entry_e1"] = entry
|
||||
@@ -411,9 +407,10 @@ class TestEntryMigration:
|
||||
# ADB options carried over byte-compatibly
|
||||
assert entry.options["adb_enabled"] is True
|
||||
assert entry.options["adb_port"] == 5555
|
||||
# Protocol fields default; host discovery (ADB fallback) stays active
|
||||
# Protocol fields default; the entry keeps its own address (t41: the v1
|
||||
# adb_host is mapped onto CONF_HOST, empty here)
|
||||
assert entry.options[CONF_PROTOCOL_ENABLED] is False
|
||||
assert entry.options[CONF_PROTOCOL_HOST] == ""
|
||||
assert entry.options[CONF_HOST] == ""
|
||||
assert hass.config_entries.updating_entry_calls == [
|
||||
(entry, {"options": entry.options, "version": 2})
|
||||
]
|
||||
|
||||
@@ -31,12 +31,10 @@ import voluptuous_mirror as vol_mirror
|
||||
from conftest import _SHIELD_DIR, _load_module
|
||||
from nsh.const import (
|
||||
CONF_ADB_ENABLED,
|
||||
CONF_ADB_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_HOST,
|
||||
CONF_PAIRING_CODE,
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
CONF_PROTOCOL_HOST,
|
||||
CONF_SKIP_PAIRING,
|
||||
DEFAULT_ADB_PORT,
|
||||
)
|
||||
@@ -390,11 +388,9 @@ def _patch_adb(monkeypatch, module, connected=True):
|
||||
def _user_input(**overrides) -> dict:
|
||||
base = {
|
||||
"name": "mdarcy",
|
||||
CONF_ANDROIDTV_ENTITY: "media_player.shield",
|
||||
CONF_ADB_ENABLED: False,
|
||||
CONF_ADB_HOST: "",
|
||||
CONF_PROTOCOL_ENABLED: False,
|
||||
CONF_PROTOCOL_HOST: "",
|
||||
CONF_HOST: "", # t41: one address field for both transports
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
@@ -444,22 +440,22 @@ class TestVoluptuousMirrorContract:
|
||||
|
||||
def test_a_default_on_the_declaration_is_visible(self):
|
||||
vol = sys.modules["voluptuous"]
|
||||
schema = {vol.Optional(CONF_ADB_HOST, default=DISCOVERED_HOST): str}
|
||||
schema = {vol.Optional(CONF_HOST, default=DISCOVERED_HOST): str}
|
||||
|
||||
assert vol_mirror.defaults_by_key(schema)[CONF_ADB_HOST] == DISCOVERED_HOST
|
||||
assert vol_mirror.defaults_by_key(schema)[CONF_HOST] == DISCOVERED_HOST
|
||||
|
||||
def test_a_later_redeclaration_cannot_add_the_default(self):
|
||||
vol = sys.modules["voluptuous"]
|
||||
# this is the t33 shape: declare bare, then "fix" it by re-declaring.
|
||||
# Real voluptuous keeps the FIRST key object, so the default is dropped.
|
||||
schema = {vol.Optional(CONF_ADB_HOST): str}
|
||||
schema[vol.Optional(CONF_ADB_HOST, default=DISCOVERED_HOST)] = str
|
||||
schema = {vol.Optional(CONF_HOST): str}
|
||||
schema[vol.Optional(CONF_HOST, default=DISCOVERED_HOST)] = str
|
||||
|
||||
assert vol_mirror.defaults_by_key(schema)[CONF_ADB_HOST] is vol_mirror.UNDEFINED
|
||||
assert vol_mirror.defaults_by_key(schema)[CONF_HOST] is vol_mirror.UNDEFINED
|
||||
|
||||
def test_a_bare_string_key_is_rejected_loudly(self):
|
||||
with pytest.raises(AssertionError):
|
||||
vol_mirror.declared_fields({CONF_ADB_HOST: str})
|
||||
vol_mirror.declared_fields({CONF_HOST: str})
|
||||
|
||||
def test_marker_equality_is_field_only_like_voluptuous(self):
|
||||
"""t35 F2: real ``Marker.__eq__`` is ``self.schema == other``.
|
||||
@@ -468,12 +464,12 @@ class TestVoluptuousMirrorContract:
|
||||
"""
|
||||
vol = sys.modules["voluptuous"]
|
||||
|
||||
assert vol.Optional(CONF_ADB_HOST) == vol.Required(CONF_ADB_HOST)
|
||||
assert hash(vol.Optional(CONF_ADB_HOST)) == hash(
|
||||
vol.Required(CONF_ADB_HOST)
|
||||
assert vol.Optional(CONF_HOST) == vol.Required(CONF_HOST)
|
||||
assert hash(vol.Optional(CONF_HOST)) == hash(
|
||||
vol.Required(CONF_HOST)
|
||||
)
|
||||
# the real implementation compares the field name to the other value
|
||||
assert vol.Optional(CONF_ADB_HOST) == CONF_ADB_HOST
|
||||
assert vol.Optional(CONF_HOST) == CONF_HOST
|
||||
|
||||
def test_a_mixed_marker_redeclaration_drops_the_default(self):
|
||||
"""The case a class-comparing mirror masks (t35 F2).
|
||||
@@ -483,11 +479,11 @@ class TestVoluptuousMirrorContract:
|
||||
schema silently loses the default, exactly like the Optional case.
|
||||
"""
|
||||
vol = sys.modules["voluptuous"]
|
||||
schema = {vol.Optional(CONF_ADB_HOST): str}
|
||||
schema[vol.Required(CONF_ADB_HOST, default=DISCOVERED_HOST)] = str
|
||||
schema = {vol.Optional(CONF_HOST): str}
|
||||
schema[vol.Required(CONF_HOST, default=DISCOVERED_HOST)] = str
|
||||
|
||||
assert (
|
||||
vol_mirror.defaults_by_key(schema)[CONF_ADB_HOST]
|
||||
vol_mirror.defaults_by_key(schema)[CONF_HOST]
|
||||
is vol_mirror.UNDEFINED
|
||||
)
|
||||
|
||||
@@ -495,7 +491,7 @@ class TestVoluptuousMirrorContract:
|
||||
class TestDiscoveryPrefill:
|
||||
"""F-A / F-A2 / F-D: the discovered address reaches the rendered FORM."""
|
||||
|
||||
async def test_discovered_host_is_the_default_of_both_host_fields(self, tmp_path):
|
||||
async def test_discovered_host_is_the_default_of_the_host_field(self, tmp_path):
|
||||
module = _form_module()
|
||||
flow, _hass = _new_flow(module, tmp_path)
|
||||
|
||||
@@ -509,23 +505,22 @@ class TestDiscoveryPrefill:
|
||||
defaults = vol_mirror.defaults_by_key(result["data_schema"])
|
||||
# schema-level, not "the attribute was stored": this is precisely what
|
||||
# the old assertion missed while the form rendered blank
|
||||
assert defaults[CONF_ADB_HOST] == DISCOVERED_HOST
|
||||
assert defaults[CONF_PROTOCOL_HOST] == DISCOVERED_HOST
|
||||
assert defaults[CONF_HOST] == DISCOVERED_HOST
|
||||
|
||||
async def test_every_host_field_carries_the_discovered_default(self, tmp_path):
|
||||
"""The keys are declared WITH the default — never re-declared bare."""
|
||||
async def test_the_host_field_carries_the_discovered_default(self, tmp_path):
|
||||
"""The key is declared WITH the default — never re-declared bare."""
|
||||
module = _form_module()
|
||||
flow, _hass = _new_flow(module, tmp_path)
|
||||
|
||||
result = await flow.async_step_zeroconf(_discovery())
|
||||
|
||||
for field in (CONF_ADB_HOST, CONF_PROTOCOL_HOST):
|
||||
keys = [
|
||||
key for key in vol_mirror.declared_keys(result["data_schema"])
|
||||
if key.schema == field
|
||||
]
|
||||
assert len(keys) == 1, f"{field} is declared {len(keys)} times"
|
||||
assert keys[0].default == DISCOVERED_HOST
|
||||
keys = [
|
||||
key
|
||||
for key in vol_mirror.declared_keys(result["data_schema"])
|
||||
if key.schema == CONF_HOST
|
||||
]
|
||||
assert len(keys) == 1, f"the address field is declared {len(keys)} times"
|
||||
assert keys[0].default == DISCOVERED_HOST
|
||||
|
||||
async def test_discovery_step_announces_the_known_address(self, tmp_path):
|
||||
module = _form_module()
|
||||
@@ -544,7 +539,7 @@ class TestDiscoveryPrefill:
|
||||
|
||||
assert result["description_placeholders"]["discovery_note"] == ""
|
||||
assert (
|
||||
vol_mirror.defaults_by_key(result["data_schema"])[CONF_ADB_HOST]
|
||||
vol_mirror.defaults_by_key(result["data_schema"])[CONF_HOST]
|
||||
is vol_mirror.UNDEFINED
|
||||
)
|
||||
|
||||
@@ -556,7 +551,7 @@ class TestDiscoveryPrefill:
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
defaults = vol_mirror.defaults_by_key(result["data_schema"])
|
||||
assert defaults[CONF_ADB_HOST] == "10.0.0.31"
|
||||
assert defaults[CONF_HOST] == "10.0.0.31"
|
||||
assert defaults["name"] == "Living"
|
||||
assert result["description_placeholders"]["discovery_note"] == ""
|
||||
|
||||
@@ -583,8 +578,7 @@ class TestDiscoveryPrefill:
|
||||
|
||||
assert result["errors"] == {"base": "no_state_source"}
|
||||
defaults = vol_mirror.defaults_by_key(result["data_schema"])
|
||||
assert defaults[CONF_ADB_HOST] == DISCOVERED_HOST
|
||||
assert defaults[CONF_PROTOCOL_HOST] == DISCOVERED_HOST
|
||||
assert defaults[CONF_HOST] == DISCOVERED_HOST
|
||||
|
||||
|
||||
class TestNoDeadEndPairing:
|
||||
@@ -604,9 +598,9 @@ class TestNoDeadEndPairing:
|
||||
assert CONF_SKIP_PAIRING in vol_mirror.declared_fields(form["data_schema"])
|
||||
|
||||
adb_fields = vol_mirror.declared_fields(adb_form["data_schema"])
|
||||
assert CONF_ADB_HOST in adb_fields # the field adb_no_host names
|
||||
assert CONF_HOST in adb_fields # the field adb_no_host names
|
||||
protocol_fields = vol_mirror.declared_fields(protocol_form["data_schema"])
|
||||
assert CONF_PROTOCOL_HOST in protocol_fields # protocol_no_host names this
|
||||
assert CONF_HOST in protocol_fields # protocol_no_host names this
|
||||
|
||||
async def test_the_skip_option_is_off_by_default(self, tmp_path, monkeypatch):
|
||||
module = _form_module()
|
||||
@@ -631,7 +625,7 @@ class TestNoDeadEndPairing:
|
||||
_user_input(
|
||||
**{
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -662,7 +656,7 @@ class TestNoDeadEndPairing:
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -695,7 +689,7 @@ class TestNoDeadEndPairing:
|
||||
**{
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -717,7 +711,7 @@ class TestNoDeadEndPairing:
|
||||
**{
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -739,7 +733,7 @@ class TestNoDeadEndPairing:
|
||||
_user_input(
|
||||
**{
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -763,7 +757,7 @@ class TestNoDeadEndPairing:
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -785,11 +779,11 @@ class TestNoDeadEndPairing:
|
||||
)
|
||||
|
||||
result = await flow.async_step_adb_pair(
|
||||
{CONF_ADB_HOST: "10.0.0.77", CONF_ADB_PORT: 5555}
|
||||
{CONF_HOST: "10.0.0.77", CONF_ADB_PORT: 5555}
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["options"][CONF_ADB_HOST] == "10.0.0.77"
|
||||
assert result["options"][CONF_HOST] == "10.0.0.77"
|
||||
assert test_connect.await_args.args[0] == "10.0.0.77"
|
||||
|
||||
async def test_protocol_form_host_correction_restarts_pairing(self, tmp_path, monkeypatch):
|
||||
@@ -801,7 +795,7 @@ class TestNoDeadEndPairing:
|
||||
_user_input(
|
||||
**{
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -814,12 +808,12 @@ class TestNoDeadEndPairing:
|
||||
monkeypatch.setattr(
|
||||
ShieldConnectionManager, "build_protocol_client", lambda *a, **k: second
|
||||
)
|
||||
form = await flow.async_step_protocol_pair({CONF_PROTOCOL_HOST: "10.0.0.77"})
|
||||
form = await flow.async_step_protocol_pair({CONF_HOST: "10.0.0.77"})
|
||||
|
||||
assert form["step_id"] == "protocol_pair"
|
||||
assert first.disconnects == 1
|
||||
assert second.pairing_started is True
|
||||
assert flow._user_data[CONF_PROTOCOL_HOST] == "10.0.0.77"
|
||||
assert flow._user_data[CONF_HOST] == "10.0.0.77"
|
||||
|
||||
async def test_adb_no_host_error_keeps_the_user_on_a_fixable_form(
|
||||
self, tmp_path, monkeypatch
|
||||
@@ -834,7 +828,7 @@ class TestNoDeadEndPairing:
|
||||
result = await flow.async_step_adb_pair({CONF_ADB_PORT: 5555})
|
||||
|
||||
assert result["errors"] == {"base": "adb_no_host"}
|
||||
assert CONF_ADB_HOST in vol_mirror.declared_fields(result["data_schema"])
|
||||
assert CONF_HOST in vol_mirror.declared_fields(result["data_schema"])
|
||||
|
||||
async def test_adb_failed_error_keeps_the_user_on_the_form(self, tmp_path, monkeypatch):
|
||||
module = _form_module()
|
||||
@@ -844,7 +838,7 @@ class TestNoDeadEndPairing:
|
||||
_user_input(
|
||||
**{
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -873,7 +867,7 @@ class TestNoDeadEndPairing:
|
||||
flow._user_data = {
|
||||
"name": "mdarcy",
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: DISCOVERED_HOST,
|
||||
CONF_HOST: DISCOVERED_HOST,
|
||||
}
|
||||
|
||||
result = await flow.async_step_protocol_pair(None)
|
||||
@@ -890,7 +884,7 @@ class TestNoDeadEndPairing:
|
||||
entry_id="e1",
|
||||
title="mdarcy",
|
||||
data={},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_PROTOCOL_HOST: DISCOVERED_HOST},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: DISCOVERED_HOST},
|
||||
)
|
||||
hass.config_entries.by_id["e1"] = entry
|
||||
flow.context = {"entry_id": "e1"}
|
||||
@@ -912,9 +906,8 @@ class TestProtocolReconfigure:
|
||||
def _entry(self, **overrides):
|
||||
options = {
|
||||
CONF_PROTOCOL_ENABLED: False, # the "continue without pairing" outcome
|
||||
CONF_PROTOCOL_HOST: "",
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: "10.0.0.9",
|
||||
CONF_HOST: "10.0.0.9", # t41: the entry's one address
|
||||
CONF_ADB_PORT: 5555,
|
||||
}
|
||||
options.update(overrides)
|
||||
@@ -944,7 +937,7 @@ class TestProtocolReconfigure:
|
||||
assert form["step_id"] == "protocol_pair" # the PIN step
|
||||
assert client.pairing_started is True
|
||||
fields = vol_mirror.declared_fields(form["data_schema"])
|
||||
assert CONF_PROTOCOL_HOST in fields # the address is editable here
|
||||
assert CONF_HOST in fields # the address is editable here
|
||||
assert CONF_PAIRING_CODE in fields
|
||||
|
||||
result = await flow.async_step_protocol_pair(
|
||||
@@ -954,10 +947,10 @@ class TestProtocolReconfigure:
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
# the transport is ON and carries the address that just paired
|
||||
assert entry.options[CONF_PROTOCOL_ENABLED] is True
|
||||
assert entry.options[CONF_PROTOCOL_HOST] == "10.0.0.9"
|
||||
assert entry.options[CONF_HOST] == "10.0.0.9"
|
||||
# the rest of the entry's options survive the update
|
||||
assert entry.options[CONF_ADB_ENABLED] is True
|
||||
assert entry.options[CONF_ADB_HOST] == "10.0.0.9"
|
||||
assert entry.options[CONF_HOST] == "10.0.0.9"
|
||||
assert entry.unique_id == FAKE_MAC
|
||||
assert hass.config_entries.reloaded_entry_ids == ["e1"]
|
||||
assert client.finished_codes == [FAKE_PAIRING_CODE]
|
||||
@@ -990,7 +983,7 @@ class TestProtocolReconfigure:
|
||||
ShieldConnectionManager, "build_protocol_client", lambda *a, **k: second
|
||||
)
|
||||
form = await flow.async_step_protocol_pair(
|
||||
{CONF_PROTOCOL_HOST: "10.0.0.77", CONF_PAIRING_CODE: "0000"}
|
||||
{CONF_HOST: "10.0.0.77", CONF_PAIRING_CODE: "0000"}
|
||||
)
|
||||
|
||||
assert form["step_id"] == "protocol_pair" # asks for the NEW code
|
||||
@@ -1003,7 +996,7 @@ class TestProtocolReconfigure:
|
||||
)
|
||||
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert entry.options[CONF_PROTOCOL_HOST] == "10.0.0.77"
|
||||
assert entry.options[CONF_HOST] == "10.0.0.77"
|
||||
|
||||
async def test_reconfigure_skip_leaves_the_entry_untouched(
|
||||
self, tmp_path, monkeypatch
|
||||
|
||||
+35
-7
@@ -119,8 +119,9 @@ class TestYamlSeedBlocks:
|
||||
"""_yaml_seed_blocks: the optional domain key → normalized seeds."""
|
||||
|
||||
def test_block_normalized(self, _integration_module):
|
||||
from nsh.const import CONF_ADB_HOST, CONF_ADB_PORT, DEFAULT_ADB_PORT
|
||||
from nsh.const import CONF_ADB_PORT, CONF_HOST, DEFAULT_ADB_PORT
|
||||
|
||||
# t41: a LEGACY block (adb_host) still seeds — normalized onto CONF_HOST
|
||||
blocks = _integration_module._yaml_seed_blocks(
|
||||
[{"name": "Living", "adb_host": "10.0.0.31"}]
|
||||
)
|
||||
@@ -128,20 +129,31 @@ class TestYamlSeedBlocks:
|
||||
assert len(blocks) == 1
|
||||
seed = blocks[0]
|
||||
assert seed["name"] == "Living"
|
||||
assert seed[CONF_ADB_HOST] == "10.0.0.31"
|
||||
assert seed[CONF_HOST] == "10.0.0.31"
|
||||
assert seed[CONF_ADB_PORT] == DEFAULT_ADB_PORT
|
||||
assert seed["adb_enabled"] is True
|
||||
assert "unique_id" not in seed # optional key only
|
||||
|
||||
def test_the_current_host_key_seeds_too(self, _integration_module):
|
||||
from nsh.const import CONF_HOST
|
||||
|
||||
seed = _integration_module._yaml_seed_blocks(
|
||||
[{"name": "Desk", CONF_HOST: "shield.local"}]
|
||||
)[0]
|
||||
|
||||
assert seed[CONF_HOST] == "shield.local"
|
||||
assert seed["adb_enabled"] is True # an address names the Shield
|
||||
|
||||
def test_single_block_and_protocol_hint(self, _integration_module):
|
||||
blocks = _integration_module._yaml_seed_blocks(
|
||||
{"name": "Desk", "protocol_host": "shield.local", "adb_port": 5000}
|
||||
)
|
||||
|
||||
assert len(blocks) == 1
|
||||
# protocol fields are prefill hints ONLY (pairing stays interactive)
|
||||
assert blocks[0]["protocol_host"] == "shield.local"
|
||||
# a legacy protocol-only block still seeds, with ADB left OFF (t32 F4)
|
||||
assert blocks[0]["host"] == "shield.local"
|
||||
assert blocks[0]["adb_port"] == 5000
|
||||
assert blocks[0]["adb_enabled"] is False
|
||||
|
||||
def test_list_and_single_block_shapes(self, _integration_module):
|
||||
single = _integration_module._yaml_seed_blocks({"name": "A"})
|
||||
@@ -166,7 +178,23 @@ class TestSeeding:
|
||||
domain, context, data = hass.config_entries.flows[0]
|
||||
assert domain == "nvidia_shield"
|
||||
assert context["source"] == "import"
|
||||
assert data["adb_host"] == "10.0.0.5"
|
||||
assert data["host"] == "10.0.0.5" # t41: one address field
|
||||
|
||||
|
||||
class TestSeedingCurrentKey:
|
||||
"""The t41 `host` key seeds exactly like the legacy one."""
|
||||
|
||||
async def test_host_key_seeds_a_flow(self, _integration_module):
|
||||
hass = _SeedHass(entries=[])
|
||||
|
||||
await _integration_module.async_setup(
|
||||
hass, {"nvidia_shield": [{"name": "H", "host": "10.0.0.7"}]}
|
||||
)
|
||||
if hass.scheduled_tasks:
|
||||
await asyncio.gather(*hass.scheduled_tasks, return_exceptions=True)
|
||||
|
||||
assert len(hass.config_entries.flows) == 1
|
||||
assert hass.config_entries.flows[0][2]["host"] == "10.0.0.7"
|
||||
|
||||
async def test_hostless_block_never_seeds(self, _integration_module):
|
||||
hass = _SeedHass(entries=[])
|
||||
@@ -229,12 +257,12 @@ class TestImportStep:
|
||||
return flow
|
||||
|
||||
async def test_ui_configured_host_never_re_seeded(self):
|
||||
from nsh.const import CONF_ADB_HOST
|
||||
from nsh.const import CONF_HOST
|
||||
|
||||
existing = SimpleNamespace(
|
||||
entry_id="ui1",
|
||||
unique_id=None,
|
||||
options={CONF_ADB_HOST: "10.0.0.31"},
|
||||
options={CONF_HOST: "10.0.0.31"},
|
||||
)
|
||||
# options live on the entry (conf zeroconf guard reads them)
|
||||
flow = self._flow([existing])
|
||||
|
||||
@@ -21,11 +21,10 @@ import voluptuous_mirror as vol_mirror
|
||||
from conftest import _SHIELD_DIR, _load_module
|
||||
from nsh.const import (
|
||||
CONF_ADB_ENABLED,
|
||||
CONF_ADB_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_HOST,
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
CONF_PROTOCOL_HOST,
|
||||
)
|
||||
|
||||
_STUB_NAMES = (
|
||||
@@ -302,7 +301,7 @@ def _user_input(**overrides) -> dict:
|
||||
base = {
|
||||
"name": "mdarcy",
|
||||
CONF_PROTOCOL_ENABLED: False,
|
||||
CONF_PROTOCOL_HOST: "",
|
||||
CONF_HOST: "",
|
||||
CONF_ADB_ENABLED: False,
|
||||
}
|
||||
base.update(overrides)
|
||||
@@ -372,7 +371,7 @@ class TestStandaloneValidation:
|
||||
flow._user_data = {
|
||||
"name": "mdarcy",
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: "shield.local",
|
||||
CONF_HOST: "shield.local",
|
||||
}
|
||||
|
||||
# skip the pairing route: drive _create_entry directly
|
||||
@@ -436,7 +435,7 @@ class TestMigrationKeepsOrAllowsAbsence:
|
||||
(
|
||||
entry,
|
||||
{
|
||||
"options": {CONF_PROTOCOL_ENABLED: False, CONF_PROTOCOL_HOST: ""},
|
||||
"options": {CONF_PROTOCOL_ENABLED: False, CONF_HOST: ""},
|
||||
"version": 2,
|
||||
},
|
||||
)
|
||||
@@ -449,7 +448,7 @@ class TestMigrationKeepsOrAllowsAbsence:
|
||||
entry = SimpleNamespace(
|
||||
version=2,
|
||||
data={},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_PROTOCOL_HOST: "shield.local"},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: "shield.local"},
|
||||
title="mdarcy",
|
||||
)
|
||||
|
||||
@@ -791,10 +790,10 @@ class TestIpChangeSelfHealF1:
|
||||
return SimpleNamespace(
|
||||
entry_id="e1",
|
||||
title="Shield",
|
||||
data={CONF_ANDROIDTV_ENTITY: "media_player.shield"},
|
||||
data={},
|
||||
options={
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: host,
|
||||
CONF_HOST: host, # t41: the entry's one address
|
||||
CONF_ADB_PORT: 5555,
|
||||
},
|
||||
runtime_data=None,
|
||||
@@ -831,11 +830,7 @@ class TestIpChangeSelfHealF1:
|
||||
|
||||
# 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",
|
||||
}
|
||||
entry.options = {**entry.options, CONF_HOST: "192.168.1.77"}
|
||||
|
||||
second = factory()
|
||||
assert second is not None
|
||||
@@ -890,7 +885,7 @@ class TestMacLessDiscoveryF2:
|
||||
entry = SimpleNamespace(
|
||||
entry_id="e1",
|
||||
unique_id=None, # MAC-less entry
|
||||
options={CONF_PROTOCOL_HOST: "192.168.1.50"},
|
||||
options={CONF_HOST: "192.168.1.50"},
|
||||
title="Shield",
|
||||
)
|
||||
self._holder().entries[:] = [entry]
|
||||
@@ -951,7 +946,7 @@ class TestSeedBlockShapeF4:
|
||||
)
|
||||
|
||||
assert blocks[0]["adb_enabled"] is False
|
||||
assert blocks[0]["adb_host"] == ""
|
||||
assert blocks[0]["host"] == "shield.local" # t41: one address field
|
||||
|
||||
def test_adb_block_keeps_adb_on(self):
|
||||
import conftest
|
||||
@@ -1006,8 +1001,7 @@ class TestZeroconfDiscovery:
|
||||
assert result["step_id"] == "user"
|
||||
assert flow._discovered_host == "192.168.1.50"
|
||||
defaults = vol_mirror.defaults_by_key(result["data_schema"])
|
||||
assert defaults[CONF_ADB_HOST] == "192.168.1.50"
|
||||
assert defaults[CONF_PROTOCOL_HOST] == "192.168.1.50"
|
||||
assert defaults[CONF_HOST] == "192.168.1.50"
|
||||
# ...and a discovery-driven form says the address is already known
|
||||
note = result["description_placeholders"]["discovery_note"]
|
||||
assert "Discovered Shield at 192.168.1.50" in note
|
||||
@@ -1031,7 +1025,7 @@ class TestZeroconfDiscovery:
|
||||
|
||||
entry = SimpleNamespace(
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_PROTOCOL_HOST: "10.0.0.1"},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: "10.0.0.1"},
|
||||
entry_id="e1",
|
||||
title="Shield",
|
||||
)
|
||||
@@ -1042,9 +1036,8 @@ class TestZeroconfDiscovery:
|
||||
)
|
||||
|
||||
assert result == {"type": "abort", "reason": "already_configured"}
|
||||
# the hosts moved to the discovery address
|
||||
assert entry.options[CONF_PROTOCOL_HOST] == "192.168.1.50"
|
||||
assert entry.options["adb_host"] == "192.168.1.50"
|
||||
# t41: the ONE address field moved to the discovery address
|
||||
assert entry.options[CONF_HOST] == "192.168.1.50"
|
||||
assert hass.config_entries.reloaded_entry_ids == [] # update-only (G3a)
|
||||
|
||||
async def test_known_mac_same_ip_skips_the_rewrite(self, tmp_path):
|
||||
@@ -1053,14 +1046,10 @@ class TestZeroconfDiscovery:
|
||||
hass = FakeHass(str(tmp_path))
|
||||
flow.hass = hass
|
||||
|
||||
# both hosts already hold the discovered address: nothing changes
|
||||
# the address already holds the discovered value: nothing changes
|
||||
entry = SimpleNamespace(
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
options={
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: "192.168.1.50",
|
||||
"adb_host": "192.168.1.50",
|
||||
},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: "192.168.1.50"},
|
||||
entry_id="e1",
|
||||
title="Shield",
|
||||
)
|
||||
@@ -1074,8 +1063,24 @@ class TestZeroconfDiscovery:
|
||||
assert hass.config_entries.updating_entry_calls == [] # no IP change
|
||||
assert hass.config_entries.reloaded_entry_ids == []
|
||||
|
||||
async def test_blank_adb_host_self_heals_from_the_discovery_address(self, tmp_path):
|
||||
"""F2: a blank (auto-detect) adb host follows a same-IP rediscovery."""
|
||||
async def test_the_per_host_heal_cases_collapsed_into_one_rule(self):
|
||||
"""t41: the blank / self-heal / deliberate-override cases are gone.
|
||||
|
||||
They only existed because the address was stored twice (t21 F2). With one
|
||||
field the discovery address simply IS the entry's address, so this pins
|
||||
the simplification: a second address field cannot quietly come back.
|
||||
"""
|
||||
source = (_SHIELD_DIR / "config_flow.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "CONF_ADB_HOST" not in source
|
||||
assert "CONF_PROTOCOL_HOST" not in source
|
||||
|
||||
async def test_an_explicit_address_follows_the_device(self, tmp_path):
|
||||
"""t41: one Shield, one address — discovery is its current source.
|
||||
|
||||
The old rule kept a "deliberately different ADB host" untouched; that
|
||||
override is precisely what the single-field ruling removes.
|
||||
"""
|
||||
module = _form_module()
|
||||
flow = module.NvidiaShieldConfigFlow()
|
||||
hass = FakeHass(str(tmp_path))
|
||||
@@ -1083,37 +1088,7 @@ class TestZeroconfDiscovery:
|
||||
|
||||
entry = SimpleNamespace(
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_PROTOCOL_HOST: "192.168.1.50"},
|
||||
entry_id="e1",
|
||||
title="Shield",
|
||||
)
|
||||
sys.modules[__name__]._zc_holder.entries[:] = [entry]
|
||||
|
||||
result = await flow.async_step_zeroconf(
|
||||
self._discovery(props={"mac": "aa:bb:cc:dd:ee:ff"})
|
||||
)
|
||||
|
||||
assert result == {"type": "abort", "reason": "already_configured"}
|
||||
# only the adb host was filled (self-heal), protocol already current:
|
||||
update = hass.config_entries.updating_entry_calls[0][1]["options"]
|
||||
assert update["adb_host"] == "192.168.1.50"
|
||||
assert update["protocol_host"] == "192.168.1.50"
|
||||
assert hass.config_entries.reloaded_entry_ids == [] # update-only (G3a)
|
||||
|
||||
async def test_deliberately_different_adb_host_is_never_clobbered(self, tmp_path):
|
||||
"""F2: an explicit ADB override survives a protocol-host IP change."""
|
||||
module = _form_module()
|
||||
flow = module.NvidiaShieldConfigFlow()
|
||||
hass = FakeHass(str(tmp_path))
|
||||
flow.hass = hass
|
||||
|
||||
entry = SimpleNamespace(
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
options={
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: "10.0.0.1", # stale (IP moved)
|
||||
"adb_host": "10.0.0.9", # deliberate ADB override
|
||||
},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: "10.0.0.9"}, # stale
|
||||
entry_id="e1",
|
||||
title="Shield",
|
||||
)
|
||||
@@ -1125,8 +1100,7 @@ class TestZeroconfDiscovery:
|
||||
|
||||
assert result == {"type": "abort", "reason": "already_configured"}
|
||||
update = hass.config_entries.updating_entry_calls[0][1]["options"]
|
||||
assert update["protocol_host"] == "192.168.1.50" # protocol followed
|
||||
assert update["adb_host"] == "10.0.0.9" # override untouched
|
||||
assert update[CONF_HOST] == "192.168.1.50"
|
||||
assert hass.config_entries.reloaded_entry_ids == [] # update-only (G3a)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user