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:
Jc2k 2019-12-21 10:22:07 +00:00 committed by Paulus Schoutsen
parent 3911f24f75
commit b41480ae46
4 changed files with 257 additions and 0 deletions

View File

@ -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.

View File

@ -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."""

View File

@ -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",
}

View File

@ -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