mirror of
https://github.com/home-assistant/core
synced 2024-08-28 03:36:46 +02:00
Add support for bluetooth local name matchers shorter than 3 chars (#107411)
This commit is contained in:
parent
69307374f4
commit
efffbc08aa
@ -237,10 +237,12 @@ class BluetoothMatcherIndexBase(Generic[_T]):
|
|||||||
def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]:
|
def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]:
|
||||||
"""Check for a match."""
|
"""Check for a match."""
|
||||||
matches = []
|
matches = []
|
||||||
if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH:
|
if (name := service_info.name) and (
|
||||||
for matcher in self.local_name.get(
|
local_name_matchers := self.local_name.get(
|
||||||
service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], []
|
name[:LOCAL_NAME_MIN_MATCH_LENGTH]
|
||||||
):
|
)
|
||||||
|
):
|
||||||
|
for matcher in local_name_matchers:
|
||||||
if ble_device_matches(matcher, service_info):
|
if ble_device_matches(matcher, service_info):
|
||||||
matches.append(matcher)
|
matches.append(matcher)
|
||||||
|
|
||||||
@ -351,11 +353,6 @@ def _local_name_to_index_key(local_name: str) -> str:
|
|||||||
if they try to setup a matcher that will is overly broad
|
if they try to setup a matcher that will is overly broad
|
||||||
as would match too many devices and cause a performance hit.
|
as would match too many devices and cause a performance hit.
|
||||||
"""
|
"""
|
||||||
if len(local_name) < LOCAL_NAME_MIN_MATCH_LENGTH:
|
|
||||||
raise ValueError(
|
|
||||||
"Local name matchers must be at least "
|
|
||||||
f"{LOCAL_NAME_MIN_MATCH_LENGTH} characters long ({local_name})"
|
|
||||||
)
|
|
||||||
match_part = local_name[:LOCAL_NAME_MIN_MATCH_LENGTH]
|
match_part = local_name[:LOCAL_NAME_MIN_MATCH_LENGTH]
|
||||||
if "*" in match_part or "[" in match_part:
|
if "*" in match_part or "[" in match_part:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -377,35 +374,29 @@ def ble_device_matches(
|
|||||||
if matcher.get(CONNECTABLE, True) and not service_info.connectable:
|
if matcher.get(CONNECTABLE, True) and not service_info.connectable:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
advertisement_data = service_info.advertisement
|
|
||||||
if (
|
if (
|
||||||
service_uuid := matcher.get(SERVICE_UUID)
|
service_uuid := matcher.get(SERVICE_UUID)
|
||||||
) and service_uuid not in advertisement_data.service_uuids:
|
) and service_uuid not in service_info.service_uuids:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
service_data_uuid := matcher.get(SERVICE_DATA_UUID)
|
service_data_uuid := matcher.get(SERVICE_DATA_UUID)
|
||||||
) and service_data_uuid not in advertisement_data.service_data:
|
) and service_data_uuid not in service_info.service_data:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if manfacturer_id := matcher.get(MANUFACTURER_ID):
|
if manufacturer_id := matcher.get(MANUFACTURER_ID):
|
||||||
if manfacturer_id not in advertisement_data.manufacturer_data:
|
if manufacturer_id not in service_info.manufacturer_data:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START):
|
if manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START):
|
||||||
manufacturer_data_start_bytes = bytearray(manufacturer_data_start)
|
if not service_info.manufacturer_data[manufacturer_id].startswith(
|
||||||
if not any(
|
bytes(manufacturer_data_start)
|
||||||
manufacturer_data.startswith(manufacturer_data_start_bytes)
|
|
||||||
for manufacturer_data in advertisement_data.manufacturer_data.values()
|
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (local_name := matcher.get(LOCAL_NAME)) and (
|
if (local_name := matcher.get(LOCAL_NAME)) and not _memorized_fnmatch(
|
||||||
(device_name := advertisement_data.local_name or service_info.device.name)
|
service_info.name,
|
||||||
is None
|
local_name,
|
||||||
or not _memorized_fnmatch(
|
|
||||||
device_name,
|
|
||||||
local_name,
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import time
|
import time
|
||||||
from unittest.mock import ANY, MagicMock, Mock, patch
|
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
from bleak import BleakError
|
from bleak import BleakError
|
||||||
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
||||||
@ -376,6 +376,56 @@ async def test_discovery_match_by_service_uuid(
|
|||||||
assert mock_config_flow.mock_calls[0][1][0] == "switchbot"
|
assert mock_config_flow.mock_calls[0][1][0] == "switchbot"
|
||||||
|
|
||||||
|
|
||||||
|
@patch.object(
|
||||||
|
bluetooth,
|
||||||
|
"async_get_bluetooth",
|
||||||
|
return_value=[
|
||||||
|
{
|
||||||
|
"domain": "sensorpush",
|
||||||
|
"local_name": "s",
|
||||||
|
"service_uuid": "ef090000-11d6-42ba-93b8-9dd7ec090aa9",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_discovery_match_by_service_uuid_and_short_local_name(
|
||||||
|
mock_async_get_bluetooth: AsyncMock,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bleak_scanner_start: MagicMock,
|
||||||
|
mock_bluetooth_adapters: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test bluetooth discovery match by service_uuid and short local name."""
|
||||||
|
entry = MockConfigEntry(domain="bluetooth", unique_id="00:00:00:00:00:01")
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
|
||||||
|
await async_setup_with_default_adapter(hass)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_bleak_scanner_start.mock_calls) == 1
|
||||||
|
|
||||||
|
wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
|
||||||
|
wrong_adv = generate_advertisement_data(local_name="s", service_uuids=[])
|
||||||
|
|
||||||
|
inject_advertisement(hass, wrong_device, wrong_adv)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
|
|
||||||
|
ht1_device = generate_ble_device("44:44:33:11:23:45", "s")
|
||||||
|
ht1_adv = generate_advertisement_data(
|
||||||
|
local_name="s", service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090aa9"]
|
||||||
|
)
|
||||||
|
|
||||||
|
inject_advertisement(hass, ht1_device, ht1_adv)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
|
assert mock_config_flow.mock_calls[0][1][0] == "sensorpush"
|
||||||
|
|
||||||
|
|
||||||
def _domains_from_mock_config_flow(mock_config_flow: Mock) -> list[str]:
|
def _domains_from_mock_config_flow(mock_config_flow: Mock) -> list[str]:
|
||||||
"""Get all the domains that were passed to async_init except bluetooth."""
|
"""Get all the domains that were passed to async_init except bluetooth."""
|
||||||
return [call[1][0] for call in mock_config_flow.mock_calls if call[1][0] != DOMAIN]
|
return [call[1][0] for call in mock_config_flow.mock_calls if call[1][0] != DOMAIN]
|
||||||
@ -2016,14 +2066,6 @@ async def test_register_callback_by_local_name_overly_broad(
|
|||||||
):
|
):
|
||||||
await async_setup_with_default_adapter(hass)
|
await async_setup_with_default_adapter(hass)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
bluetooth.async_register_callback(
|
|
||||||
hass,
|
|
||||||
_fake_subscriber,
|
|
||||||
{LOCAL_NAME: "a"},
|
|
||||||
BluetoothScanningMode.ACTIVE,
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
bluetooth.async_register_callback(
|
bluetooth.async_register_callback(
|
||||||
hass,
|
hass,
|
||||||
|
Loading…
Reference in New Issue
Block a user