mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47:10 +00:00
Add a config entry mechanism to rediscover a discovery that was ignored (#30099)
* Mechanism to rediscover a discovery that was ignored * Add core config entry tests for new rediscover step * Add tests for homekit_controller implementation of async_step_rediscover * Rename rediscover to unignore * Comment the new ignore/unignore mechanisms
This commit is contained in:
parent
3911f24f75
commit
b41480ae46
@ -108,6 +108,38 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_unignore(self, user_input):
|
||||
"""Rediscover a previously ignored discover."""
|
||||
unique_id = user_input["unique_id"]
|
||||
await self.async_set_unique_id(unique_id)
|
||||
|
||||
records = await self.hass.async_add_executor_job(self.controller.discover, 5)
|
||||
for record in records:
|
||||
if normalize_hkid(record["id"]) != unique_id:
|
||||
continue
|
||||
return await self.async_step_zeroconf(
|
||||
{
|
||||
"host": record["address"],
|
||||
"port": record["port"],
|
||||
"hostname": record["name"],
|
||||
"type": "_hap._tcp.local.",
|
||||
"name": record["name"],
|
||||
"properties": {
|
||||
"md": record["md"],
|
||||
"pv": record["pv"],
|
||||
"id": unique_id,
|
||||
"c#": record["c#"],
|
||||
"s#": record["s#"],
|
||||
"ff": record["ff"],
|
||||
"ci": record["ci"],
|
||||
"sf": record["sf"],
|
||||
"sh": "",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_abort(reason="no_devices")
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle a discovered HomeKit accessory.
|
||||
|
||||
|
@ -24,8 +24,17 @@ SOURCE_IMPORT = "import"
|
||||
SOURCE_SSDP = "ssdp"
|
||||
SOURCE_USER = "user"
|
||||
SOURCE_ZEROCONF = "zeroconf"
|
||||
|
||||
# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow
|
||||
# websocket command creates a config entry with this source and while it exists normal discoveries
|
||||
# with the same unique id are ignored.
|
||||
SOURCE_IGNORE = "ignore"
|
||||
|
||||
# This is used when a user uses the "Stop Ignoring" button in the UI (the
|
||||
# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has
|
||||
# been removed and unloaded.
|
||||
SOURCE_UNIGNORE = "unignore"
|
||||
|
||||
HANDLERS = Registry()
|
||||
|
||||
STORAGE_KEY = "core.config_entries"
|
||||
@ -461,6 +470,19 @@ class ConfigEntries:
|
||||
dev_reg.async_clear_config_entry(entry_id)
|
||||
ent_reg.async_clear_config_entry(entry_id)
|
||||
|
||||
# After we have fully removed an "ignore" config entry we can try and rediscover it so that a
|
||||
# user is able to immediately start configuring it. We do this by starting a new flow with
|
||||
# the 'unignore' step. If the integration doesn't implement async_step_unignore then
|
||||
# this will be a no-op.
|
||||
if entry.source == SOURCE_IGNORE:
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.flow.async_init(
|
||||
entry.domain,
|
||||
context={"source": SOURCE_UNIGNORE},
|
||||
data={"unique_id": entry.unique_id},
|
||||
)
|
||||
)
|
||||
|
||||
return {"require_restart": not unload_success}
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
@ -827,6 +849,10 @@ class ConfigFlow(data_entry_flow.FlowHandler):
|
||||
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
|
||||
return self.async_create_entry(title="Ignored", data={})
|
||||
|
||||
async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Rediscover a config entry by it's unique_id."""
|
||||
return self.async_abort(reason="not_implemented")
|
||||
|
||||
|
||||
class OptionsFlowManager:
|
||||
"""Flow to set options for a configuration entry."""
|
||||
|
@ -836,3 +836,85 @@ async def test_parse_overlapping_homekit_json(hass):
|
||||
"title_placeholders": {"name": "TestDevice"},
|
||||
"unique_id": "00:00:00:00:00:00",
|
||||
}
|
||||
|
||||
|
||||
async def test_unignore_works(hass):
|
||||
"""Test rediscovery triggered disovers work."""
|
||||
discovery_info = {
|
||||
"name": "TestDevice",
|
||||
"address": "127.0.0.1",
|
||||
"port": 8080,
|
||||
"md": "TestDevice",
|
||||
"pv": "1.0",
|
||||
"id": "00:00:00:00:00:00",
|
||||
"c#": 1,
|
||||
"s#": 1,
|
||||
"ff": 0,
|
||||
"ci": 0,
|
||||
"sf": 1,
|
||||
}
|
||||
|
||||
pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"})
|
||||
pairing.list_accessories_and_characteristics.return_value = [
|
||||
{
|
||||
"aid": 1,
|
||||
"services": [
|
||||
{
|
||||
"characteristics": [{"type": "23", "value": "Koogeek-LS1-20833F"}],
|
||||
"type": "3e",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
flow = _setup_flow_handler(hass)
|
||||
|
||||
flow.controller.pairings = {"00:00:00:00:00:00": pairing}
|
||||
flow.controller.discover.return_value = [discovery_info]
|
||||
|
||||
result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:00"})
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "pair"
|
||||
assert flow.context == {
|
||||
"hkid": "00:00:00:00:00:00",
|
||||
"title_placeholders": {"name": "TestDevice"},
|
||||
"unique_id": "00:00:00:00:00:00",
|
||||
}
|
||||
|
||||
# User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code
|
||||
result = await flow.async_step_pair({})
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "pair"
|
||||
assert flow.controller.start_pairing.call_count == 1
|
||||
|
||||
# Pairing finalized
|
||||
result = await flow.async_step_pair({"pairing_code": "111-22-33"})
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "Koogeek-LS1-20833F"
|
||||
assert result["data"] == pairing.pairing_data
|
||||
|
||||
|
||||
async def test_unignore_ignores_missing_devices(hass):
|
||||
"""Test rediscovery triggered disovers handle devices that have gone away."""
|
||||
discovery_info = {
|
||||
"name": "TestDevice",
|
||||
"address": "127.0.0.1",
|
||||
"port": 8080,
|
||||
"md": "TestDevice",
|
||||
"pv": "1.0",
|
||||
"id": "00:00:00:00:00:00",
|
||||
"c#": 1,
|
||||
"s#": 1,
|
||||
"ff": 0,
|
||||
"ci": 0,
|
||||
"sf": 1,
|
||||
}
|
||||
|
||||
flow = _setup_flow_handler(hass)
|
||||
flow.controller.discover.return_value = [discovery_info]
|
||||
|
||||
result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:01"})
|
||||
assert result["type"] == "abort"
|
||||
assert flow.context == {
|
||||
"unique_id": "00:00:00:00:00:01",
|
||||
}
|
||||
|
@ -1186,3 +1186,120 @@ async def test_unique_id_ignore(hass, manager):
|
||||
|
||||
assert entry.source == "ignore"
|
||||
assert entry.unique_id == "mock-unique-id"
|
||||
|
||||
|
||||
async def test_unignore_step_form(hass, manager):
|
||||
"""Test that we can ignore flows that are in progress and have a unique ID, then rediscover them."""
|
||||
async_setup_entry = MagicMock(return_value=mock_coro(True))
|
||||
mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_unignore(self, user_input):
|
||||
unique_id = user_input["unique_id"]
|
||||
await self.async_set_unique_id(unique_id)
|
||||
return self.async_show_form(step_id="discovery")
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
result = await manager.flow.async_init(
|
||||
"comp",
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
data={"unique_id": "mock-unique-id"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
entry = hass.config_entries.async_entries("comp")[0]
|
||||
assert entry.source == "ignore"
|
||||
assert entry.unique_id == "mock-unique-id"
|
||||
assert entry.domain == "comp"
|
||||
|
||||
await manager.async_remove(entry.entry_id)
|
||||
|
||||
# Right after removal there shouldn't be an entry or active flows
|
||||
assert len(hass.config_entries.async_entries("comp")) == 0
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
|
||||
# But after a 'tick' the unignore step has run and we can see an active flow again.
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.config_entries.flow.async_progress()) == 1
|
||||
|
||||
# and still not config entries
|
||||
assert len(hass.config_entries.async_entries("comp")) == 0
|
||||
|
||||
|
||||
async def test_unignore_create_entry(hass, manager):
|
||||
"""Test that we can ignore flows that are in progress and have a unique ID, then rediscover them."""
|
||||
async_setup_entry = MagicMock(return_value=mock_coro(True))
|
||||
mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_unignore(self, user_input):
|
||||
unique_id = user_input["unique_id"]
|
||||
await self.async_set_unique_id(unique_id)
|
||||
return self.async_create_entry(title="yo", data={})
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
result = await manager.flow.async_init(
|
||||
"comp",
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
data={"unique_id": "mock-unique-id"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
entry = hass.config_entries.async_entries("comp")[0]
|
||||
assert entry.source == "ignore"
|
||||
assert entry.unique_id == "mock-unique-id"
|
||||
assert entry.domain == "comp"
|
||||
|
||||
await manager.async_remove(entry.entry_id)
|
||||
|
||||
# Right after removal there shouldn't be an entry or flow
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
assert len(hass.config_entries.async_entries("comp")) == 0
|
||||
|
||||
# But after a 'tick' the unignore step has run and we can see a config entry.
|
||||
await hass.async_block_till_done()
|
||||
entry = hass.config_entries.async_entries("comp")[0]
|
||||
assert entry.source == "unignore"
|
||||
assert entry.unique_id == "mock-unique-id"
|
||||
assert entry.title == "yo"
|
||||
|
||||
# And still no active flow
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
|
||||
|
||||
async def test_unignore_default_impl(hass, manager):
|
||||
"""Test that resdicovery is a no-op by default."""
|
||||
async_setup_entry = MagicMock(return_value=mock_coro(True))
|
||||
mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
result = await manager.flow.async_init(
|
||||
"comp",
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
data={"unique_id": "mock-unique-id"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
entry = hass.config_entries.async_entries("comp")[0]
|
||||
assert entry.source == "ignore"
|
||||
assert entry.unique_id == "mock-unique-id"
|
||||
assert entry.domain == "comp"
|
||||
|
||||
await manager.async_remove(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.config_entries.async_entries("comp")) == 0
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
|
Loading…
x
Reference in New Issue
Block a user