mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Remove unignore flow (#126765)
This commit is contained in:
parent
f53411b95a
commit
7d61cb1ef5
@ -106,11 +106,6 @@ SOURCE_ZEROCONF = "zeroconf"
|
|||||||
# source and while it exists normal discoveries with the same unique id are ignored.
|
# source and while it exists normal discoveries with the same unique id are ignored.
|
||||||
SOURCE_IGNORE = "ignore"
|
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"
|
|
||||||
|
|
||||||
# This is used to signal that re-authentication is required by the user.
|
# This is used to signal that re-authentication is required by the user.
|
||||||
SOURCE_REAUTH = "reauth"
|
SOURCE_REAUTH = "reauth"
|
||||||
|
|
||||||
@ -179,7 +174,6 @@ DISCOVERY_SOURCES = {
|
|||||||
SOURCE_INTEGRATION_DISCOVERY,
|
SOURCE_INTEGRATION_DISCOVERY,
|
||||||
SOURCE_MQTT,
|
SOURCE_MQTT,
|
||||||
SOURCE_SSDP,
|
SOURCE_SSDP,
|
||||||
SOURCE_UNIGNORE,
|
|
||||||
SOURCE_USB,
|
SOURCE_USB,
|
||||||
SOURCE_ZEROCONF,
|
SOURCE_ZEROCONF,
|
||||||
}
|
}
|
||||||
@ -1264,7 +1258,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]):
|
|||||||
# a single config entry, but which already has an entry
|
# a single config entry, but which already has an entry
|
||||||
if (
|
if (
|
||||||
context.get("source")
|
context.get("source")
|
||||||
not in {SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_UNIGNORE, SOURCE_RECONFIGURE}
|
not in {SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE}
|
||||||
and self.config_entries.async_has_entries(handler, include_ignore=False)
|
and self.config_entries.async_has_entries(handler, include_ignore=False)
|
||||||
and await _support_single_config_entry_only(self.hass, handler)
|
and await _support_single_config_entry_only(self.hass, handler)
|
||||||
):
|
):
|
||||||
@ -1855,20 +1849,6 @@ class ConfigEntries:
|
|||||||
issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}"
|
issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}"
|
||||||
ir.async_delete_issue(self.hass, HOMEASSISTANT_DOMAIN, issue_id)
|
ir.async_delete_issue(self.hass, HOMEASSISTANT_DOMAIN, issue_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_internal(
|
|
||||||
self.hass.config_entries.flow.async_init(
|
|
||||||
entry.domain,
|
|
||||||
context={"source": SOURCE_UNIGNORE},
|
|
||||||
data={"unique_id": entry.unique_id},
|
|
||||||
),
|
|
||||||
f"config entry unignore {entry.title} {entry.domain} {entry.unique_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._async_dispatch(ConfigEntryChange.REMOVED, entry)
|
self._async_dispatch(ConfigEntryChange.REMOVED, entry)
|
||||||
for discovery_domain in entry.discovery_keys:
|
for discovery_domain in entry.discovery_keys:
|
||||||
async_dispatcher_send_internal(
|
async_dispatcher_send_internal(
|
||||||
@ -2544,10 +2524,6 @@ class ConfigFlow(ConfigEntryBaseFlow):
|
|||||||
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
|
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
|
||||||
return self.async_create_entry(title=user_input["title"], data={})
|
return self.async_create_entry(title=user_input["title"], data={})
|
||||||
|
|
||||||
async def async_step_unignore(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
|
||||||
"""Rediscover a config entry by it's unique_id."""
|
|
||||||
return self.async_abort(reason="not_implemented")
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
@ -1335,7 +1335,14 @@ async def test_set_fallback_interval_big(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("entry_source", [config_entries.SOURCE_IGNORE])
|
@pytest.mark.parametrize(
|
||||||
|
"entry_source",
|
||||||
|
[
|
||||||
|
config_entries.SOURCE_BLUETOOTH,
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_bluetooth_rediscover(
|
async def test_bluetooth_rediscover(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_domain: str,
|
entry_domain: str,
|
||||||
@ -1386,205 +1393,6 @@ async def test_bluetooth_rediscover(
|
|||||||
BluetoothScanningMode.ACTIVE,
|
BluetoothScanningMode.ACTIVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class FakeScanner(BaseHaRemoteScanner):
|
|
||||||
def inject_advertisement(
|
|
||||||
self, device: BLEDevice, advertisement_data: AdvertisementData
|
|
||||||
) -> None:
|
|
||||||
"""Inject an advertisement."""
|
|
||||||
self._async_on_advertisement(
|
|
||||||
device.address,
|
|
||||||
advertisement_data.rssi,
|
|
||||||
device.name,
|
|
||||||
advertisement_data.service_uuids,
|
|
||||||
advertisement_data.service_data,
|
|
||||||
advertisement_data.manufacturer_data,
|
|
||||||
advertisement_data.tx_power,
|
|
||||||
{"scanner_specific_data": "test"},
|
|
||||||
MONOTONIC_TIME(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def clear_all_devices(self) -> None:
|
|
||||||
"""Clear all devices."""
|
|
||||||
self._discovered_device_advertisement_datas.clear()
|
|
||||||
self._discovered_device_timestamps.clear()
|
|
||||||
self._previous_service_info.clear()
|
|
||||||
|
|
||||||
connector = (
|
|
||||||
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
|
||||||
)
|
|
||||||
non_connectable_scanner = FakeScanner(
|
|
||||||
"connectable",
|
|
||||||
"connectable",
|
|
||||||
connector,
|
|
||||||
False,
|
|
||||||
)
|
|
||||||
unsetup_connectable_scanner = non_connectable_scanner.async_setup()
|
|
||||||
cancel_connectable_scanner = _get_manager().async_register_scanner(
|
|
||||||
non_connectable_scanner
|
|
||||||
)
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
|
|
||||||
non_connectable_scanner.inject_advertisement(
|
|
||||||
switchbot_device_non_connectable, switchbot_device_adv
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
expected_context = {
|
|
||||||
"discovery_key": DiscoveryKey(
|
|
||||||
domain="bluetooth", key="44:44:33:11:23:45", version=1
|
|
||||||
),
|
|
||||||
"source": "bluetooth",
|
|
||||||
}
|
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
|
||||||
assert mock_config_flow.mock_calls[0][1][0] == "switchbot"
|
|
||||||
assert mock_config_flow.mock_calls[0][2]["context"] == expected_context
|
|
||||||
|
|
||||||
hass.config.components.add(entry_domain)
|
|
||||||
mock_integration(hass, MockModule(entry_domain))
|
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain=entry_domain,
|
|
||||||
discovery_keys=entry_discovery_keys,
|
|
||||||
unique_id="mock-unique-id",
|
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
|
||||||
source=entry_source,
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
assert (
|
|
||||||
async_ble_device_from_address(hass, "44:44:33:11:23:45", False) is not None
|
|
||||||
)
|
|
||||||
assert async_scanner_count(hass, connectable=False) == 1
|
|
||||||
assert len(callbacks) == 1
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"44:44:33:11:23:45"
|
|
||||||
in non_connectable_scanner.discovered_devices_and_advertisement_data
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert (
|
|
||||||
async_ble_device_from_address(hass, "44:44:33:11:23:45", False) is not None
|
|
||||||
)
|
|
||||||
assert async_scanner_count(hass, connectable=False) == 1
|
|
||||||
assert len(callbacks) == 1
|
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 3
|
|
||||||
assert mock_config_flow.mock_calls[1][1][0] == entry_domain
|
|
||||||
assert mock_config_flow.mock_calls[1][2]["context"] == {
|
|
||||||
"source": "unignore",
|
|
||||||
}
|
|
||||||
assert mock_config_flow.mock_calls[2][1][0] == "switchbot"
|
|
||||||
assert mock_config_flow.mock_calls[2][2]["context"] == expected_context
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
unsetup_connectable_scanner()
|
|
||||||
cancel_connectable_scanner()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_bluetooth_adapters")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
(
|
|
||||||
"entry_domain",
|
|
||||||
"entry_discovery_keys",
|
|
||||||
),
|
|
||||||
[
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"switchbot",
|
|
||||||
{
|
|
||||||
"bluetooth": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="bluetooth", key="44:44:33:11:23:45", version=1
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"switchbot",
|
|
||||||
{
|
|
||||||
"bluetooth": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="bluetooth", key="44:44:33:11:23:45", version=1
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"other": (DiscoveryKey(domain="other", key="blah", version=1),),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# Matching discovery key, other domain
|
|
||||||
# Note: Rediscovery is not currently restricted to the domain of the removed
|
|
||||||
# entry. Such a check can be added if needed.
|
|
||||||
(
|
|
||||||
"comp",
|
|
||||||
{
|
|
||||||
"bluetooth": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="bluetooth", key="44:44:33:11:23:45", version=1
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"entry_source", [config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_USER]
|
|
||||||
)
|
|
||||||
async def test_bluetooth_rediscover_2(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry_domain: str,
|
|
||||||
entry_discovery_keys: tuple,
|
|
||||||
entry_source: str,
|
|
||||||
) -> None:
|
|
||||||
"""Test we reinitiate flows when an ignored config entry is removed.
|
|
||||||
|
|
||||||
This test can be merged with test_zeroconf_rediscover when
|
|
||||||
async_step_unignore has been removed from the ConfigFlow base class.
|
|
||||||
"""
|
|
||||||
mock_bt = [
|
|
||||||
{
|
|
||||||
"domain": "switchbot",
|
|
||||||
"service_data_uuid": "050a021a-0000-1000-8000-00805f9b34fb",
|
|
||||||
"connectable": False,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
|
|
||||||
):
|
|
||||||
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert async_scanner_count(hass, connectable=False) == 0
|
|
||||||
switchbot_device_non_connectable = generate_ble_device(
|
|
||||||
"44:44:33:11:23:45",
|
|
||||||
"wohand",
|
|
||||||
{},
|
|
||||||
rssi=-100,
|
|
||||||
)
|
|
||||||
switchbot_device_adv = generate_advertisement_data(
|
|
||||||
local_name="wohand",
|
|
||||||
service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"],
|
|
||||||
service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"},
|
|
||||||
manufacturer_data={1: b"\x01"},
|
|
||||||
rssi=-100,
|
|
||||||
)
|
|
||||||
callbacks = []
|
|
||||||
|
|
||||||
def _fake_subscriber(
|
|
||||||
service_info: BluetoothServiceInfo,
|
|
||||||
change: BluetoothChange,
|
|
||||||
) -> None:
|
|
||||||
"""Fake subscriber for the BleakScanner."""
|
|
||||||
callbacks.append((service_info, change))
|
|
||||||
|
|
||||||
cancel = bluetooth.async_register_callback(
|
|
||||||
hass,
|
|
||||||
_fake_subscriber,
|
|
||||||
{"address": "44:44:33:11:23:45", "connectable": False},
|
|
||||||
BluetoothScanningMode.ACTIVE,
|
|
||||||
)
|
|
||||||
|
|
||||||
class FakeScanner(BaseHaRemoteScanner):
|
class FakeScanner(BaseHaRemoteScanner):
|
||||||
def inject_advertisement(
|
def inject_advertisement(
|
||||||
self, device: BLEDevice, advertisement_data: AdvertisementData
|
self, device: BLEDevice, advertisement_data: AdvertisementData
|
||||||
@ -1847,12 +1655,7 @@ async def test_bluetooth_rediscover_no_match(
|
|||||||
)
|
)
|
||||||
assert async_scanner_count(hass, connectable=False) == 1
|
assert async_scanner_count(hass, connectable=False) == 1
|
||||||
assert len(callbacks) == 1
|
assert len(callbacks) == 1
|
||||||
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
assert len(mock_config_flow.mock_calls) == 2
|
|
||||||
assert mock_config_flow.mock_calls[1][1][0] == entry_domain
|
|
||||||
assert mock_config_flow.mock_calls[1][2]["context"] == {
|
|
||||||
"source": "unignore",
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
unsetup_connectable_scanner()
|
unsetup_connectable_scanner()
|
||||||
|
@ -1201,7 +1201,14 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) -
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("entry_source", [config_entries.SOURCE_IGNORE])
|
@pytest.mark.parametrize(
|
||||||
|
"entry_source",
|
||||||
|
[
|
||||||
|
config_entries.SOURCE_DHCP,
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_dhcp_rediscover(
|
async def test_dhcp_rediscover(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_domain: str,
|
entry_domain: str,
|
||||||
@ -1255,105 +1262,6 @@ async def test_dhcp_rediscover(
|
|||||||
macaddress="b8b7f16db533",
|
macaddress="b8b7f16db533",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 2
|
|
||||||
assert mock_init.mock_calls[0][1][0] == entry_domain
|
|
||||||
assert mock_init.mock_calls[0][2]["context"] == {"source": "unignore"}
|
|
||||||
assert mock_init.mock_calls[1][1][0] == "mock-domain"
|
|
||||||
assert mock_init.mock_calls[1][2]["context"] == expected_context
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
(
|
|
||||||
"entry_domain",
|
|
||||||
"entry_discovery_keys",
|
|
||||||
),
|
|
||||||
[
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"mock-domain",
|
|
||||||
{"dhcp": (DiscoveryKey(domain="dhcp", key="b8b7f16db533", version=1),)},
|
|
||||||
),
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"mock-domain",
|
|
||||||
{
|
|
||||||
"dhcp": (DiscoveryKey(domain="dhcp", key="b8b7f16db533", version=1),),
|
|
||||||
"other": (DiscoveryKey(domain="other", key="blah", version=1),),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# Matching discovery key, other domain
|
|
||||||
# Note: Rediscovery is not currently restricted to the domain of the removed
|
|
||||||
# entry. Such a check can be added if needed.
|
|
||||||
(
|
|
||||||
"comp",
|
|
||||||
{"dhcp": (DiscoveryKey(domain="dhcp", key="b8b7f16db533", version=1),)},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"entry_source", [config_entries.SOURCE_USER, config_entries.SOURCE_ZEROCONF]
|
|
||||||
)
|
|
||||||
async def test_dhcp_rediscover_2(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry_domain: str,
|
|
||||||
entry_discovery_keys: tuple,
|
|
||||||
entry_source: str,
|
|
||||||
) -> None:
|
|
||||||
"""Test we reinitiate flows when an ignored config entry is removed.
|
|
||||||
|
|
||||||
This test can be merged with test_zeroconf_rediscover when
|
|
||||||
async_step_unignore has been removed from the ConfigFlow base class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain=entry_domain,
|
|
||||||
discovery_keys=entry_discovery_keys,
|
|
||||||
unique_id="mock-unique-id",
|
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
|
||||||
source=entry_source,
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
address_data = {}
|
|
||||||
integration_matchers = dhcp.async_index_integration_matchers(
|
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}]
|
|
||||||
)
|
|
||||||
packet = Ether(RAW_DHCP_REQUEST)
|
|
||||||
|
|
||||||
async_handle_dhcp_packet = await _async_get_handle_dhcp_packet(
|
|
||||||
hass, integration_matchers, address_data
|
|
||||||
)
|
|
||||||
rediscovery_watcher = dhcp.RediscoveryWatcher(
|
|
||||||
hass, address_data, integration_matchers
|
|
||||||
)
|
|
||||||
rediscovery_watcher.async_start()
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
|
||||||
await async_handle_dhcp_packet(packet)
|
|
||||||
# Ensure no change is ignored
|
|
||||||
await async_handle_dhcp_packet(packet)
|
|
||||||
|
|
||||||
# Assert the cached MAC address is hexstring without :
|
|
||||||
assert address_data == {
|
|
||||||
"b8b7f16db533": {"hostname": "connect", "ip": "192.168.210.56"}
|
|
||||||
}
|
|
||||||
|
|
||||||
expected_context = {
|
|
||||||
"discovery_key": DiscoveryKey(domain="dhcp", key="b8b7f16db533", version=1),
|
|
||||||
"source": config_entries.SOURCE_DHCP,
|
|
||||||
}
|
|
||||||
assert len(mock_init.mock_calls) == 1
|
|
||||||
assert mock_init.mock_calls[0][1][0] == "mock-domain"
|
|
||||||
assert mock_init.mock_calls[0][2]["context"] == expected_context
|
|
||||||
assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo(
|
|
||||||
ip="192.168.210.56",
|
|
||||||
hostname="connect",
|
|
||||||
macaddress="b8b7f16db533",
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1447,6 +1355,4 @@ async def test_dhcp_rediscover_no_match(
|
|||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 1
|
assert len(mock_init.mock_calls) == 0
|
||||||
assert mock_init.mock_calls[0][1][0] == entry_domain
|
|
||||||
assert mock_init.mock_calls[0][2]["context"] == {"source": "unignore"}
|
|
||||||
|
@ -938,7 +938,14 @@ async def test_flow_dismiss_on_byebye(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("entry_source", [config_entries.SOURCE_IGNORE])
|
@pytest.mark.parametrize(
|
||||||
|
"entry_source",
|
||||||
|
[
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
config_entries.SOURCE_SSDP,
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_ssdp_rediscover(
|
async def test_ssdp_rediscover(
|
||||||
mock_get_ssdp,
|
mock_get_ssdp,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -999,116 +1006,6 @@ async def test_ssdp_rediscover(
|
|||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_flow_init.mock_calls) == 3
|
|
||||||
assert mock_flow_init.mock_calls[1][1][0] == entry_domain
|
|
||||||
assert mock_flow_init.mock_calls[1][2]["context"] == {"source": "unignore"}
|
|
||||||
assert mock_flow_init.mock_calls[2][1][0] == "mock-domain"
|
|
||||||
assert mock_flow_init.mock_calls[2][2]["context"] == expected_context
|
|
||||||
assert (
|
|
||||||
mock_flow_init.mock_calls[2][2]["data"]
|
|
||||||
== mock_flow_init.mock_calls[0][2]["data"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@patch(
|
|
||||||
"homeassistant.components.ssdp.async_get_ssdp",
|
|
||||||
return_value={"mock-domain": [{"st": "mock-st"}]},
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
(
|
|
||||||
"entry_domain",
|
|
||||||
"entry_discovery_keys",
|
|
||||||
),
|
|
||||||
[
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"mock-domain",
|
|
||||||
{"ssdp": (DiscoveryKey(domain="ssdp", key="uuid:mock-udn", version=1),)},
|
|
||||||
),
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"mock-domain",
|
|
||||||
{
|
|
||||||
"ssdp": (DiscoveryKey(domain="ssdp", key="uuid:mock-udn", version=1),),
|
|
||||||
"other": (DiscoveryKey(domain="other", key="blah", version=1),),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# Matching discovery key, other domain
|
|
||||||
# Note: Rediscovery is not currently restricted to the domain of the removed
|
|
||||||
# entry. Such a check can be added if needed.
|
|
||||||
(
|
|
||||||
"comp",
|
|
||||||
{"ssdp": (DiscoveryKey(domain="ssdp", key="uuid:mock-udn", version=1),)},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"entry_source", [config_entries.SOURCE_USER, config_entries.SOURCE_ZEROCONF]
|
|
||||||
)
|
|
||||||
async def test_ssdp_rediscover_2(
|
|
||||||
mock_get_ssdp,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
aioclient_mock: AiohttpClientMocker,
|
|
||||||
mock_flow_init,
|
|
||||||
entry_domain: str,
|
|
||||||
entry_discovery_keys: tuple,
|
|
||||||
entry_source: str,
|
|
||||||
) -> None:
|
|
||||||
"""Test we reinitiate flows when an ignored config entry is removed.
|
|
||||||
|
|
||||||
This test can be merged with test_zeroconf_rediscover when
|
|
||||||
async_step_unignore has been removed from the ConfigFlow base class.
|
|
||||||
"""
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain=entry_domain,
|
|
||||||
discovery_keys=entry_discovery_keys,
|
|
||||||
unique_id="mock-unique-id",
|
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
|
||||||
source=entry_source,
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
mock_ssdp_search_response = _ssdp_headers(
|
|
||||||
{
|
|
||||||
"st": "mock-st",
|
|
||||||
"location": "http://1.1.1.1",
|
|
||||||
"usn": "uuid:mock-udn::mock-st",
|
|
||||||
"server": "mock-server",
|
|
||||||
"ext": "",
|
|
||||||
"_source": "search",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
aioclient_mock.get(
|
|
||||||
"http://1.1.1.1",
|
|
||||||
text="""
|
|
||||||
<root>
|
|
||||||
<device>
|
|
||||||
<deviceType>Paulus</deviceType>
|
|
||||||
<manufacturer>Paulus</manufacturer>
|
|
||||||
</device>
|
|
||||||
</root>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
ssdp_listener = await init_ssdp_component(hass)
|
|
||||||
ssdp_listener._on_search(mock_ssdp_search_response)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
expected_context = {
|
|
||||||
"discovery_key": DiscoveryKey(domain="ssdp", key="uuid:mock-udn", version=1),
|
|
||||||
"source": config_entries.SOURCE_SSDP,
|
|
||||||
}
|
|
||||||
assert len(mock_flow_init.mock_calls) == 1
|
|
||||||
assert mock_flow_init.mock_calls[0][1][0] == "mock-domain"
|
|
||||||
assert mock_flow_init.mock_calls[0][2]["context"] == expected_context
|
|
||||||
mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"]
|
|
||||||
assert mock_call_data.ssdp_st == "mock-st"
|
|
||||||
assert mock_call_data.ssdp_location == "http://1.1.1.1"
|
|
||||||
|
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert len(mock_flow_init.mock_calls) == 2
|
assert len(mock_flow_init.mock_calls) == 2
|
||||||
assert mock_flow_init.mock_calls[1][1][0] == "mock-domain"
|
assert mock_flow_init.mock_calls[1][1][0] == "mock-domain"
|
||||||
assert mock_flow_init.mock_calls[1][2]["context"] == expected_context
|
assert mock_flow_init.mock_calls[1][2]["context"] == expected_context
|
||||||
@ -1196,6 +1093,4 @@ async def test_ssdp_rediscover_no_match(
|
|||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_flow_init.mock_calls) == 2
|
assert len(mock_flow_init.mock_calls) == 1
|
||||||
assert mock_flow_init.mock_calls[1][1][0] == entry_domain
|
|
||||||
assert mock_flow_init.mock_calls[1][2]["context"] == {"source": "unignore"}
|
|
||||||
|
@ -1456,7 +1456,14 @@ async def test_zeroconf_removed(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("entry_source", [config_entries.SOURCE_IGNORE])
|
@pytest.mark.parametrize(
|
||||||
|
"entry_source",
|
||||||
|
[
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
config_entries.SOURCE_ZEROCONF,
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_zeroconf_rediscover(
|
async def test_zeroconf_rediscover(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_domain: str,
|
entry_domain: str,
|
||||||
@ -1483,149 +1490,6 @@ async def test_zeroconf_rediscover(
|
|||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
with (
|
|
||||||
patch.dict(
|
|
||||||
zc_gen.ZEROCONF,
|
|
||||||
{
|
|
||||||
"_http._tcp.local.": [
|
|
||||||
{
|
|
||||||
"domain": "shelly",
|
|
||||||
"name": "shelly*",
|
|
||||||
"properties": {"macaddress": "ffaadd*"},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
clear=True,
|
|
||||||
),
|
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
|
||||||
patch.object(
|
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
|
||||||
) as mock_service_browser,
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
|
||||||
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
|
||||||
),
|
|
||||||
):
|
|
||||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
expected_context = {
|
|
||||||
"discovery_key": DiscoveryKey(
|
|
||||||
domain="zeroconf",
|
|
||||||
key=("_http._tcp.local.", "Shelly108._http._tcp.local."),
|
|
||||||
version=1,
|
|
||||||
),
|
|
||||||
"source": "zeroconf",
|
|
||||||
}
|
|
||||||
assert len(mock_service_browser.mock_calls) == 1
|
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
|
||||||
assert mock_config_flow.mock_calls[0][1][0] == "shelly"
|
|
||||||
assert mock_config_flow.mock_calls[0][2]["context"] == expected_context
|
|
||||||
|
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert len(mock_service_browser.mock_calls) == 1
|
|
||||||
assert len(mock_config_flow.mock_calls) == 3
|
|
||||||
assert mock_config_flow.mock_calls[1][1][0] == entry_domain
|
|
||||||
assert mock_config_flow.mock_calls[1][2]["context"] == {
|
|
||||||
"source": "unignore",
|
|
||||||
}
|
|
||||||
assert mock_config_flow.mock_calls[2][1][0] == "shelly"
|
|
||||||
assert mock_config_flow.mock_calls[2][2]["context"] == expected_context
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_async_zeroconf")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
(
|
|
||||||
"entry_domain",
|
|
||||||
"entry_discovery_keys",
|
|
||||||
),
|
|
||||||
[
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"shelly",
|
|
||||||
{
|
|
||||||
"zeroconf": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="zeroconf",
|
|
||||||
key=("_http._tcp.local.", "Shelly108._http._tcp.local."),
|
|
||||||
version=1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# Matching discovery key
|
|
||||||
(
|
|
||||||
"shelly",
|
|
||||||
{
|
|
||||||
"zeroconf": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="zeroconf",
|
|
||||||
key=("_http._tcp.local.", "Shelly108._http._tcp.local."),
|
|
||||||
version=1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"other": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="other",
|
|
||||||
key="blah",
|
|
||||||
version=1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# Matching discovery key, other domain
|
|
||||||
# Note: Rediscovery is not currently restricted to the domain of the removed
|
|
||||||
# entry. Such a check can be added if needed.
|
|
||||||
(
|
|
||||||
"comp",
|
|
||||||
{
|
|
||||||
"zeroconf": (
|
|
||||||
DiscoveryKey(
|
|
||||||
domain="zeroconf",
|
|
||||||
key=("_http._tcp.local.", "Shelly108._http._tcp.local."),
|
|
||||||
version=1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"entry_source", [config_entries.SOURCE_USER, config_entries.SOURCE_ZEROCONF]
|
|
||||||
)
|
|
||||||
async def test_zeroconf_rediscover_2(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry_domain: str,
|
|
||||||
entry_discovery_keys: tuple,
|
|
||||||
entry_source: str,
|
|
||||||
) -> None:
|
|
||||||
"""Test we reinitiate flows when an ignored config entry is removed.
|
|
||||||
|
|
||||||
This test can be merged with test_zeroconf_rediscover when
|
|
||||||
async_step_unignore has been removed from the ConfigFlow base class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def http_only_service_update_mock(zeroconf, services, handlers):
|
|
||||||
"""Call service update handler."""
|
|
||||||
handlers[0](
|
|
||||||
zeroconf,
|
|
||||||
"_http._tcp.local.",
|
|
||||||
"Shelly108._http._tcp.local.",
|
|
||||||
ServiceStateChange.Added,
|
|
||||||
)
|
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain=entry_domain,
|
|
||||||
discovery_keys=entry_discovery_keys,
|
|
||||||
unique_id="mock-unique-id",
|
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
|
||||||
source=entry_source,
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch.dict(
|
patch.dict(
|
||||||
zc_gen.ZEROCONF,
|
zc_gen.ZEROCONF,
|
||||||
@ -1790,8 +1654,4 @@ async def test_zeroconf_rediscover_no_match(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_service_browser.mock_calls) == 1
|
assert len(mock_service_browser.mock_calls) == 1
|
||||||
assert len(mock_config_flow.mock_calls) == 2
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
assert mock_config_flow.mock_calls[1][1][0] == entry_domain
|
|
||||||
assert mock_config_flow.mock_calls[1][2]["context"] == {
|
|
||||||
"source": "unignore",
|
|
||||||
}
|
|
||||||
|
@ -3271,129 +3271,6 @@ async def test_async_current_entries_explicit_include_ignore(
|
|||||||
assert len(mock_setup_entry.mock_calls) == 0
|
assert len(mock_setup_entry.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_unignore_step_form(
|
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
|
||||||
) -> None:
|
|
||||||
"""Test that we can ignore flows that are in progress and have a unique ID, then rediscover them."""
|
|
||||||
async_setup_entry = AsyncMock(return_value=True)
|
|
||||||
mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry))
|
|
||||||
mock_platform(hass, "comp.config_flow", None)
|
|
||||||
|
|
||||||
class TestFlow(config_entries.ConfigFlow):
|
|
||||||
"""Test flow."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
async def async_step_unignore(self, user_input):
|
|
||||||
"""Test unignore step."""
|
|
||||||
unique_id = user_input["unique_id"]
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
return self.async_show_form(step_id="discovery")
|
|
||||||
|
|
||||||
with mock_config_flow("comp", TestFlow):
|
|
||||||
result = await manager.flow.async_init(
|
|
||||||
"comp",
|
|
||||||
context={"source": config_entries.SOURCE_IGNORE},
|
|
||||||
data={"unique_id": "mock-unique-id", "title": "Ignored Title"},
|
|
||||||
)
|
|
||||||
assert result["type"] == data_entry_flow.FlowResultType.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"
|
|
||||||
assert entry.title == "Ignored Title"
|
|
||||||
|
|
||||||
await manager.async_remove(entry.entry_id)
|
|
||||||
|
|
||||||
# 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_by_handler("comp")) == 1
|
|
||||||
|
|
||||||
# and still not config entries
|
|
||||||
assert len(hass.config_entries.async_entries("comp")) == 0
|
|
||||||
|
|
||||||
|
|
||||||
async def test_unignore_create_entry(
|
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
|
||||||
) -> None:
|
|
||||||
"""Test that we can ignore flows that are in progress and have a unique ID, then rediscover them."""
|
|
||||||
async_setup_entry = AsyncMock(return_value=True)
|
|
||||||
mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry))
|
|
||||||
mock_platform(hass, "comp.config_flow", None)
|
|
||||||
|
|
||||||
class TestFlow(config_entries.ConfigFlow):
|
|
||||||
"""Test flow."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
async def async_step_unignore(self, user_input):
|
|
||||||
"""Test unignore step."""
|
|
||||||
unique_id = user_input["unique_id"]
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
return self.async_create_entry(title="yo", data={})
|
|
||||||
|
|
||||||
with mock_config_flow("comp", TestFlow):
|
|
||||||
result = await manager.flow.async_init(
|
|
||||||
"comp",
|
|
||||||
context={"source": config_entries.SOURCE_IGNORE},
|
|
||||||
data={"unique_id": "mock-unique-id", "title": "Ignored Title"},
|
|
||||||
)
|
|
||||||
assert result["type"] == data_entry_flow.FlowResultType.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"
|
|
||||||
assert entry.title == "Ignored Title"
|
|
||||||
|
|
||||||
await manager.async_remove(entry.entry_id)
|
|
||||||
|
|
||||||
# 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 == config_entries.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_by_handler("comp")) == 0
|
|
||||||
|
|
||||||
|
|
||||||
async def test_unignore_default_impl(
|
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
|
||||||
) -> None:
|
|
||||||
"""Test that resdicovery is a no-op by default."""
|
|
||||||
async_setup_entry = AsyncMock(return_value=True)
|
|
||||||
mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry))
|
|
||||||
mock_platform(hass, "comp.config_flow", None)
|
|
||||||
|
|
||||||
class TestFlow(config_entries.ConfigFlow):
|
|
||||||
"""Test flow."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
with mock_config_flow("comp", TestFlow):
|
|
||||||
result = await manager.flow.async_init(
|
|
||||||
"comp",
|
|
||||||
context={"source": config_entries.SOURCE_IGNORE},
|
|
||||||
data={"unique_id": "mock-unique-id", "title": "Ignored Title"},
|
|
||||||
)
|
|
||||||
assert result["type"] == data_entry_flow.FlowResultType.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"
|
|
||||||
assert entry.title == "Ignored Title"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def test_partial_flows_hidden(
|
async def test_partial_flows_hidden(
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -5396,11 +5273,6 @@ async def test_hashable_non_string_unique_id(
|
|||||||
None,
|
None,
|
||||||
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
config_entries.SOURCE_UNIGNORE,
|
|
||||||
None,
|
|
||||||
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_USER,
|
config_entries.SOURCE_USER,
|
||||||
None,
|
None,
|
||||||
@ -5485,11 +5357,6 @@ async def test_starting_config_flow_on_single_config_entry(
|
|||||||
None,
|
None,
|
||||||
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
config_entries.SOURCE_UNIGNORE,
|
|
||||||
None,
|
|
||||||
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_USER,
|
config_entries.SOURCE_USER,
|
||||||
None,
|
None,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user