mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Reinitialize bluetooth discovery flow on config entry removal (#126555)
* Reinitialize bluetooth discovery flow on unignore * Update homeassistant/components/bluetooth/manager.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update tests * Rediscover on any removed config entry --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
b9c28bed19
commit
b6fe3a3022
@ -20,7 +20,9 @@ from homeassistant.core import (
|
|||||||
callback as hass_callback,
|
callback as hass_callback,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import discovery_flow
|
from homeassistant.helpers import discovery_flow
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .match import (
|
from .match import (
|
||||||
ADDRESS,
|
ADDRESS,
|
||||||
CALLBACK,
|
CALLBACK,
|
||||||
@ -75,12 +77,18 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
|||||||
self, service_info: BluetoothServiceInfoBleak
|
self, service_info: BluetoothServiceInfoBleak
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Trigger discovery for matching domains."""
|
"""Trigger discovery for matching domains."""
|
||||||
|
discovery_key = discovery_flow.DiscoveryKey(
|
||||||
|
domain=DOMAIN,
|
||||||
|
key=service_info.address,
|
||||||
|
version=1,
|
||||||
|
)
|
||||||
for domain in self._integration_matcher.match_domains(service_info):
|
for domain in self._integration_matcher.match_domains(service_info):
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
self.hass,
|
self.hass,
|
||||||
domain,
|
domain,
|
||||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||||
service_info,
|
service_info,
|
||||||
|
discovery_key=discovery_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
@ -110,12 +118,21 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
|||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Error in bluetooth callback")
|
_LOGGER.exception("Error in bluetooth callback")
|
||||||
|
|
||||||
|
if not matched_domains:
|
||||||
|
return # avoid creating DiscoveryKey if there are no matches
|
||||||
|
|
||||||
|
discovery_key = discovery_flow.DiscoveryKey(
|
||||||
|
domain=DOMAIN,
|
||||||
|
key=service_info.address,
|
||||||
|
version=1,
|
||||||
|
)
|
||||||
for domain in matched_domains:
|
for domain in matched_domains:
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
self.hass,
|
self.hass,
|
||||||
domain,
|
domain,
|
||||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||||
service_info,
|
service_info,
|
||||||
|
discovery_key=discovery_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _address_disappeared(self, address: str) -> None:
|
def _address_disappeared(self, address: str) -> None:
|
||||||
@ -145,6 +162,11 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
|||||||
continue
|
continue
|
||||||
seen.add(address)
|
seen.add(address)
|
||||||
self._async_trigger_matching_discovery(service_info)
|
self._async_trigger_matching_discovery(service_info)
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
config_entries.signal_discovered_config_entry_removed(DOMAIN),
|
||||||
|
self._handle_config_entry_removed,
|
||||||
|
)
|
||||||
|
|
||||||
def async_register_callback(
|
def async_register_callback(
|
||||||
self,
|
self,
|
||||||
@ -230,3 +252,16 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
|||||||
|
|
||||||
unregister = super().async_register_scanner(scanner, connection_slots)
|
unregister = super().async_register_scanner(scanner, connection_slots)
|
||||||
return partial(self._async_unregister_scanner, scanner, unregister)
|
return partial(self._async_unregister_scanner, scanner, unregister)
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def _handle_config_entry_removed(
|
||||||
|
self,
|
||||||
|
entry: config_entries.ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Handle config entry changes."""
|
||||||
|
for discovery_key in entry.discovery_keys[DOMAIN]:
|
||||||
|
if discovery_key.version != 1 or not isinstance(discovery_key.key, str):
|
||||||
|
continue
|
||||||
|
address = discovery_key.key
|
||||||
|
_LOGGER.debug("Rediscover address %s", address)
|
||||||
|
self.async_rediscover_address(address)
|
||||||
|
@ -13,6 +13,7 @@ from bluetooth_adapters import AdvertisementHistory
|
|||||||
from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS
|
from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
@ -36,6 +37,7 @@ from homeassistant.components.bluetooth.const import (
|
|||||||
UNAVAILABLE_TRACK_SECONDS,
|
UNAVAILABLE_TRACK_SECONDS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.discovery_flow import DiscoveryKey
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.json import json_loads
|
from homeassistant.util.json import json_loads
|
||||||
@ -52,7 +54,13 @@ from . import (
|
|||||||
patch_bluetooth_time,
|
patch_bluetooth_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed, load_fixture
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
MockModule,
|
||||||
|
async_fire_time_changed,
|
||||||
|
load_fixture,
|
||||||
|
mock_integration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -1002,6 +1010,12 @@ async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable(
|
|||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
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][1][0] == "switchbot"
|
||||||
|
assert mock_config_flow.mock_calls[0][2]["context"] == {
|
||||||
|
"discovery_key": DiscoveryKey(
|
||||||
|
domain="bluetooth", key="44:44:33:11:23:45", version=1
|
||||||
|
),
|
||||||
|
"source": "bluetooth",
|
||||||
|
}
|
||||||
|
|
||||||
assert async_ble_device_from_address(hass, "44:44:33:11:23:45", False) is not None
|
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 async_scanner_count(hass, connectable=False) == 1
|
||||||
@ -1075,6 +1089,12 @@ async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable(
|
|||||||
)
|
)
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
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][1][0] == "switchbot"
|
||||||
|
assert mock_config_flow.mock_calls[0][2]["context"] == {
|
||||||
|
"discovery_key": DiscoveryKey(
|
||||||
|
domain="bluetooth", key="44:44:33:11:23:45", version=1
|
||||||
|
),
|
||||||
|
"source": "bluetooth",
|
||||||
|
}
|
||||||
|
|
||||||
cancel_unavailable()
|
cancel_unavailable()
|
||||||
|
|
||||||
@ -1268,3 +1288,572 @@ async def test_set_fallback_interval_big(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
# We should forget fallback interval after it expires
|
# We should forget fallback interval after it expires
|
||||||
assert async_get_fallback_availability_interval(hass, "44:44:33:11:23:12") is None
|
assert async_get_fallback_availability_interval(hass, "44:44:33:11:23:12") is None
|
||||||
|
|
||||||
|
|
||||||
|
@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_IGNORE])
|
||||||
|
async def test_bluetooth_rediscover(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_domain: str,
|
||||||
|
entry_discovery_keys: tuple,
|
||||||
|
entry_source: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test we reinitiate flows when an ignored config entry is removed."""
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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) == 2
|
||||||
|
assert mock_config_flow.mock_calls[1][1][0] == "switchbot"
|
||||||
|
assert mock_config_flow.mock_calls[1][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",
|
||||||
|
"entry_source",
|
||||||
|
"entry_unique_id",
|
||||||
|
),
|
||||||
|
[
|
||||||
|
# Discovery key from other domain
|
||||||
|
(
|
||||||
|
"switchbot",
|
||||||
|
{
|
||||||
|
"zeroconf": (
|
||||||
|
DiscoveryKey(domain="zeroconf", key="44:44:33:11:23:45", version=1),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
"mock-unique-id",
|
||||||
|
),
|
||||||
|
# Discovery key from the future
|
||||||
|
(
|
||||||
|
"switchbot",
|
||||||
|
{
|
||||||
|
"bluetooth": (
|
||||||
|
DiscoveryKey(
|
||||||
|
domain="bluetooth", key="44:44:33:11:23:45", version=2
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
"mock-unique-id",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_bluetooth_rediscover_no_match(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_domain: str,
|
||||||
|
entry_discovery_keys: tuple,
|
||||||
|
entry_source: str,
|
||||||
|
entry_unique_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test we don't reinitiate flows when a non matching config entry is removed."""
|
||||||
|
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):
|
||||||
|
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=entry_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) == 2
|
||||||
|
assert mock_config_flow.mock_calls[1][1][0] == entry_domain
|
||||||
|
assert mock_config_flow.mock_calls[1][2]["context"] == {
|
||||||
|
"source": "unignore",
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
unsetup_connectable_scanner()
|
||||||
|
cancel_connectable_scanner()
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
FlowResultSnapshot({
|
FlowResultSnapshot({
|
||||||
'context': dict({
|
'context': dict({
|
||||||
'confirm_only': True,
|
'confirm_only': True,
|
||||||
|
'discovery_key': dict({
|
||||||
|
'domain': 'bluetooth',
|
||||||
|
'key': '00000000-0000-0000-0000-000000000001',
|
||||||
|
'version': 1,
|
||||||
|
}),
|
||||||
'source': 'bluetooth',
|
'source': 'bluetooth',
|
||||||
'title_placeholders': dict({
|
'title_placeholders': dict({
|
||||||
'name': 'Gardena Water Computer',
|
'name': 'Gardena Water Computer',
|
||||||
@ -18,6 +23,11 @@
|
|||||||
FlowResultSnapshot({
|
FlowResultSnapshot({
|
||||||
'context': dict({
|
'context': dict({
|
||||||
'confirm_only': True,
|
'confirm_only': True,
|
||||||
|
'discovery_key': dict({
|
||||||
|
'domain': 'bluetooth',
|
||||||
|
'key': '00000000-0000-0000-0000-000000000001',
|
||||||
|
'version': 1,
|
||||||
|
}),
|
||||||
'source': 'bluetooth',
|
'source': 'bluetooth',
|
||||||
'title_placeholders': dict({
|
'title_placeholders': dict({
|
||||||
'name': 'Gardena Water Computer',
|
'name': 'Gardena Water Computer',
|
||||||
@ -40,6 +50,13 @@
|
|||||||
}),
|
}),
|
||||||
'disabled_by': None,
|
'disabled_by': None,
|
||||||
'discovery_keys': dict({
|
'discovery_keys': dict({
|
||||||
|
'bluetooth': tuple(
|
||||||
|
dict({
|
||||||
|
'domain': 'bluetooth',
|
||||||
|
'key': '00000000-0000-0000-0000-000000000001',
|
||||||
|
'version': 1,
|
||||||
|
}),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
'domain': 'gardena_bluetooth',
|
'domain': 'gardena_bluetooth',
|
||||||
'entry_id': <ANY>,
|
'entry_id': <ANY>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user