2022-07-24 21:39:53 +00:00
|
|
|
"""The bluetooth integration matchers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
2022-08-23 14:35:20 +00:00
|
|
|
from fnmatch import translate
|
|
|
|
from functools import lru_cache
|
|
|
|
import re
|
2022-08-27 03:07:51 +00:00
|
|
|
from typing import TYPE_CHECKING, Final, Generic, TypedDict, TypeVar
|
2022-07-24 21:39:53 +00:00
|
|
|
|
|
|
|
from lru import LRU # pylint: disable=no-name-in-module
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
from homeassistant.core import callback
|
2022-07-24 21:39:53 +00:00
|
|
|
from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
from .models import BluetoothCallback, BluetoothServiceInfoBleak
|
2022-08-22 18:02:26 +00:00
|
|
|
|
2022-08-01 15:54:06 +00:00
|
|
|
if TYPE_CHECKING:
|
2022-08-05 12:49:34 +00:00
|
|
|
from collections.abc import MutableMapping
|
2022-08-01 15:54:06 +00:00
|
|
|
|
|
|
|
from bleak.backends.scanner import AdvertisementData
|
|
|
|
|
|
|
|
|
2022-07-24 21:39:53 +00:00
|
|
|
MAX_REMEMBER_ADDRESSES: Final = 2048
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
CALLBACK: Final = "callback"
|
|
|
|
DOMAIN: Final = "domain"
|
2022-07-24 21:39:53 +00:00
|
|
|
ADDRESS: Final = "address"
|
2022-08-22 18:02:26 +00:00
|
|
|
CONNECTABLE: Final = "connectable"
|
2022-07-24 21:39:53 +00:00
|
|
|
LOCAL_NAME: Final = "local_name"
|
|
|
|
SERVICE_UUID: Final = "service_uuid"
|
|
|
|
SERVICE_DATA_UUID: Final = "service_data_uuid"
|
|
|
|
MANUFACTURER_ID: Final = "manufacturer_id"
|
|
|
|
MANUFACTURER_DATA_START: Final = "manufacturer_data_start"
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
LOCAL_NAME_MIN_MATCH_LENGTH = 3
|
|
|
|
|
2022-07-24 21:39:53 +00:00
|
|
|
|
|
|
|
class BluetoothCallbackMatcherOptional(TypedDict, total=False):
|
|
|
|
"""Matcher for the bluetooth integration for callback optional fields."""
|
|
|
|
|
|
|
|
address: str
|
|
|
|
|
|
|
|
|
|
|
|
class BluetoothCallbackMatcher(
|
|
|
|
BluetoothMatcherOptional,
|
|
|
|
BluetoothCallbackMatcherOptional,
|
|
|
|
):
|
|
|
|
"""Callback matcher for the bluetooth integration."""
|
|
|
|
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
class _BluetoothCallbackMatcherWithCallback(TypedDict):
|
|
|
|
"""Callback for the bluetooth integration."""
|
|
|
|
|
|
|
|
callback: BluetoothCallback
|
|
|
|
|
|
|
|
|
|
|
|
class BluetoothCallbackMatcherWithCallback(
|
|
|
|
_BluetoothCallbackMatcherWithCallback,
|
|
|
|
BluetoothCallbackMatcher,
|
|
|
|
):
|
|
|
|
"""Callback matcher for the bluetooth integration that stores the callback."""
|
|
|
|
|
|
|
|
|
2023-04-14 18:22:39 +00:00
|
|
|
@dataclass(slots=True, frozen=False)
|
2022-07-24 21:39:53 +00:00
|
|
|
class IntegrationMatchHistory:
|
|
|
|
"""Track which fields have been seen."""
|
|
|
|
|
|
|
|
manufacturer_data: bool
|
2022-08-24 22:36:21 +00:00
|
|
|
service_data: set[str]
|
|
|
|
service_uuids: set[str]
|
2022-07-24 21:39:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
def seen_all_fields(
|
2022-08-22 18:02:26 +00:00
|
|
|
previous_match: IntegrationMatchHistory, advertisement_data: AdvertisementData
|
2022-07-24 21:39:53 +00:00
|
|
|
) -> bool:
|
|
|
|
"""Return if we have seen all fields."""
|
2022-08-22 18:02:26 +00:00
|
|
|
if not previous_match.manufacturer_data and advertisement_data.manufacturer_data:
|
2022-07-24 21:39:53 +00:00
|
|
|
return False
|
2022-08-24 22:36:21 +00:00
|
|
|
if advertisement_data.service_data and (
|
|
|
|
not previous_match.service_data
|
|
|
|
or not previous_match.service_data.issuperset(advertisement_data.service_data)
|
|
|
|
):
|
2022-07-24 21:39:53 +00:00
|
|
|
return False
|
2022-08-24 22:36:21 +00:00
|
|
|
if advertisement_data.service_uuids and (
|
|
|
|
not previous_match.service_uuids
|
|
|
|
or not previous_match.service_uuids.issuperset(advertisement_data.service_uuids)
|
|
|
|
):
|
2022-07-24 21:39:53 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class IntegrationMatcher:
|
|
|
|
"""Integration matcher for the bluetooth integration."""
|
|
|
|
|
|
|
|
def __init__(self, integration_matchers: list[BluetoothMatcher]) -> None:
|
|
|
|
"""Initialize the matcher."""
|
|
|
|
self._integration_matchers = integration_matchers
|
|
|
|
# Some devices use a random address so we need to use
|
|
|
|
# an LRU to avoid memory issues.
|
2022-08-05 12:49:34 +00:00
|
|
|
self._matched: MutableMapping[str, IntegrationMatchHistory] = LRU(
|
2022-07-24 21:39:53 +00:00
|
|
|
MAX_REMEMBER_ADDRESSES
|
|
|
|
)
|
2022-08-22 18:02:26 +00:00
|
|
|
self._matched_connectable: MutableMapping[str, IntegrationMatchHistory] = LRU(
|
|
|
|
MAX_REMEMBER_ADDRESSES
|
|
|
|
)
|
2022-08-27 03:07:51 +00:00
|
|
|
self._index = BluetoothMatcherIndex()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_setup(self) -> None:
|
|
|
|
"""Set up the matcher."""
|
|
|
|
for matcher in self._integration_matchers:
|
|
|
|
self._index.add(matcher)
|
|
|
|
self._index.build()
|
2022-07-24 21:39:53 +00:00
|
|
|
|
2022-08-05 12:49:34 +00:00
|
|
|
def async_clear_address(self, address: str) -> None:
|
|
|
|
"""Clear the history matches for a set of domains."""
|
|
|
|
self._matched.pop(address, None)
|
2022-08-22 18:02:26 +00:00
|
|
|
self._matched_connectable.pop(address, None)
|
|
|
|
|
|
|
|
def match_domains(self, service_info: BluetoothServiceInfoBleak) -> set[str]:
|
2022-07-24 21:39:53 +00:00
|
|
|
"""Return the domains that are matched."""
|
2022-08-22 18:02:26 +00:00
|
|
|
device = service_info.device
|
|
|
|
advertisement_data = service_info.advertisement
|
2022-08-27 03:07:51 +00:00
|
|
|
connectable = service_info.connectable
|
|
|
|
matched = self._matched_connectable if connectable else self._matched
|
2022-07-24 21:39:53 +00:00
|
|
|
matched_domains: set[str] = set()
|
2022-08-22 18:02:26 +00:00
|
|
|
if (previous_match := matched.get(device.address)) and seen_all_fields(
|
|
|
|
previous_match, advertisement_data
|
2022-07-24 21:39:53 +00:00
|
|
|
):
|
|
|
|
# We have seen all fields so we can skip the rest of the matchers
|
|
|
|
return matched_domains
|
|
|
|
matched_domains = {
|
2022-08-27 03:07:51 +00:00
|
|
|
matcher[DOMAIN] for matcher in self._index.match(service_info)
|
2022-07-24 21:39:53 +00:00
|
|
|
}
|
|
|
|
if not matched_domains:
|
|
|
|
return matched_domains
|
|
|
|
if previous_match:
|
2022-08-22 18:02:26 +00:00
|
|
|
previous_match.manufacturer_data |= bool(
|
|
|
|
advertisement_data.manufacturer_data
|
|
|
|
)
|
2022-08-24 22:36:21 +00:00
|
|
|
previous_match.service_data |= set(advertisement_data.service_data)
|
|
|
|
previous_match.service_uuids |= set(advertisement_data.service_uuids)
|
2022-07-24 21:39:53 +00:00
|
|
|
else:
|
2022-08-22 18:02:26 +00:00
|
|
|
matched[device.address] = IntegrationMatchHistory(
|
|
|
|
manufacturer_data=bool(advertisement_data.manufacturer_data),
|
2022-08-24 22:36:21 +00:00
|
|
|
service_data=set(advertisement_data.service_data),
|
|
|
|
service_uuids=set(advertisement_data.service_uuids),
|
2022-07-24 21:39:53 +00:00
|
|
|
)
|
|
|
|
return matched_domains
|
|
|
|
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
_T = TypeVar("_T", BluetoothMatcher, BluetoothCallbackMatcherWithCallback)
|
|
|
|
|
|
|
|
|
|
|
|
class BluetoothMatcherIndexBase(Generic[_T]):
|
|
|
|
"""Bluetooth matcher base for the bluetooth integration.
|
|
|
|
|
|
|
|
The indexer puts each matcher in the bucket that it is most
|
|
|
|
likely to match. This allows us to only check the service infos
|
|
|
|
against each bucket to see if we should match against the data.
|
|
|
|
|
2022-08-27 13:23:47 +00:00
|
|
|
This is optimized for cases when no service infos will be matched in
|
2022-08-27 03:07:51 +00:00
|
|
|
any bucket and we can quickly reject the service info as not matching.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
"""Initialize the matcher index."""
|
|
|
|
self.local_name: dict[str, list[_T]] = {}
|
|
|
|
self.service_uuid: dict[str, list[_T]] = {}
|
|
|
|
self.service_data_uuid: dict[str, list[_T]] = {}
|
|
|
|
self.manufacturer_id: dict[int, list[_T]] = {}
|
|
|
|
self.service_uuid_set: set[str] = set()
|
|
|
|
self.service_data_uuid_set: set[str] = set()
|
|
|
|
self.manufacturer_id_set: set[int] = set()
|
|
|
|
|
2022-09-18 15:22:54 +00:00
|
|
|
def add(self, matcher: _T) -> bool:
|
2022-08-27 03:07:51 +00:00
|
|
|
"""Add a matcher to the index.
|
|
|
|
|
|
|
|
Matchers must end up only in one bucket.
|
|
|
|
|
|
|
|
We put them in the bucket that they are most likely to match.
|
|
|
|
"""
|
2022-09-06 23:12:32 +00:00
|
|
|
# Local name is the cheapest to match since its just a dict lookup
|
2022-08-27 03:07:51 +00:00
|
|
|
if LOCAL_NAME in matcher:
|
|
|
|
self.local_name.setdefault(
|
|
|
|
_local_name_to_index_key(matcher[LOCAL_NAME]), []
|
|
|
|
).append(matcher)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-06 23:12:32 +00:00
|
|
|
# Manufacturer data is 2nd cheapest since its all ints
|
|
|
|
if MANUFACTURER_ID in matcher:
|
|
|
|
self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append(
|
|
|
|
matcher
|
|
|
|
)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
2022-09-06 23:12:32 +00:00
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
if SERVICE_UUID in matcher:
|
|
|
|
self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
2022-08-27 03:07:51 +00:00
|
|
|
|
|
|
|
if SERVICE_DATA_UUID in matcher:
|
|
|
|
self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append(
|
|
|
|
matcher
|
|
|
|
)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-18 15:22:54 +00:00
|
|
|
def remove(self, matcher: _T) -> bool:
|
2022-08-27 03:07:51 +00:00
|
|
|
"""Remove a matcher from the index.
|
|
|
|
|
|
|
|
Matchers only end up in one bucket, so once we have
|
|
|
|
removed one, we are done.
|
|
|
|
"""
|
|
|
|
if LOCAL_NAME in matcher:
|
|
|
|
self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove(
|
|
|
|
matcher
|
|
|
|
)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-06 23:12:32 +00:00
|
|
|
if MANUFACTURER_ID in matcher:
|
|
|
|
self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
2022-09-06 23:12:32 +00:00
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
if SERVICE_UUID in matcher:
|
|
|
|
self.service_uuid[matcher[SERVICE_UUID]].remove(matcher)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
2022-08-27 03:07:51 +00:00
|
|
|
|
|
|
|
if SERVICE_DATA_UUID in matcher:
|
|
|
|
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher)
|
2022-09-18 15:22:54 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2022-08-27 03:07:51 +00:00
|
|
|
|
|
|
|
def build(self) -> None:
|
|
|
|
"""Rebuild the index sets."""
|
|
|
|
self.service_uuid_set = set(self.service_uuid)
|
|
|
|
self.service_data_uuid_set = set(self.service_data_uuid)
|
|
|
|
self.manufacturer_id_set = set(self.manufacturer_id)
|
|
|
|
|
|
|
|
def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]:
|
|
|
|
"""Check for a match."""
|
|
|
|
matches = []
|
2022-09-06 23:12:32 +00:00
|
|
|
if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH:
|
2022-08-27 03:07:51 +00:00
|
|
|
for matcher in self.local_name.get(
|
|
|
|
service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], []
|
|
|
|
):
|
|
|
|
if ble_device_matches(matcher, service_info):
|
|
|
|
matches.append(matcher)
|
|
|
|
|
2022-09-06 23:12:32 +00:00
|
|
|
if self.service_data_uuid_set and service_info.service_data:
|
|
|
|
for service_data_uuid in self.service_data_uuid_set.intersection(
|
|
|
|
service_info.service_data
|
|
|
|
):
|
|
|
|
for matcher in self.service_data_uuid[service_data_uuid]:
|
|
|
|
if ble_device_matches(matcher, service_info):
|
|
|
|
matches.append(matcher)
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-06 23:12:32 +00:00
|
|
|
if self.manufacturer_id_set and service_info.manufacturer_data:
|
|
|
|
for manufacturer_id in self.manufacturer_id_set.intersection(
|
|
|
|
service_info.manufacturer_data
|
|
|
|
):
|
|
|
|
for matcher in self.manufacturer_id[manufacturer_id]:
|
|
|
|
if ble_device_matches(matcher, service_info):
|
|
|
|
matches.append(matcher)
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-06 23:12:32 +00:00
|
|
|
if self.service_uuid_set and service_info.service_uuids:
|
|
|
|
for service_uuid in self.service_uuid_set.intersection(
|
|
|
|
service_info.service_uuids
|
|
|
|
):
|
|
|
|
for matcher in self.service_uuid[service_uuid]:
|
|
|
|
if ble_device_matches(matcher, service_info):
|
|
|
|
matches.append(matcher)
|
2022-08-27 03:07:51 +00:00
|
|
|
|
|
|
|
return matches
|
|
|
|
|
|
|
|
|
|
|
|
class BluetoothMatcherIndex(BluetoothMatcherIndexBase[BluetoothMatcher]):
|
|
|
|
"""Bluetooth matcher for the bluetooth integration."""
|
|
|
|
|
|
|
|
|
|
|
|
class BluetoothCallbackMatcherIndex(
|
|
|
|
BluetoothMatcherIndexBase[BluetoothCallbackMatcherWithCallback]
|
|
|
|
):
|
2023-01-08 21:20:02 +00:00
|
|
|
"""Bluetooth matcher for the bluetooth integration.
|
|
|
|
|
|
|
|
Supports matching on addresses.
|
|
|
|
"""
|
2022-08-27 03:07:51 +00:00
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
"""Initialize the matcher index."""
|
|
|
|
super().__init__()
|
|
|
|
self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {}
|
2022-09-18 15:22:54 +00:00
|
|
|
self.connectable: list[BluetoothCallbackMatcherWithCallback] = []
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-18 15:22:54 +00:00
|
|
|
def add_callback_matcher(
|
|
|
|
self, matcher: BluetoothCallbackMatcherWithCallback
|
|
|
|
) -> None:
|
2022-08-27 03:07:51 +00:00
|
|
|
"""Add a matcher to the index.
|
|
|
|
|
|
|
|
Matchers must end up only in one bucket.
|
|
|
|
|
|
|
|
We put them in the bucket that they are most likely to match.
|
|
|
|
"""
|
|
|
|
if ADDRESS in matcher:
|
|
|
|
self.address.setdefault(matcher[ADDRESS], []).append(matcher)
|
|
|
|
return
|
|
|
|
|
2022-09-18 15:22:54 +00:00
|
|
|
if super().add(matcher):
|
|
|
|
self.build()
|
|
|
|
return
|
|
|
|
|
|
|
|
if CONNECTABLE in matcher:
|
|
|
|
self.connectable.append(matcher)
|
|
|
|
return
|
2022-08-27 03:07:51 +00:00
|
|
|
|
2022-09-18 15:22:54 +00:00
|
|
|
def remove_callback_matcher(
|
2022-08-27 03:07:51 +00:00
|
|
|
self, matcher: BluetoothCallbackMatcherWithCallback
|
|
|
|
) -> None:
|
|
|
|
"""Remove a matcher from the index.
|
|
|
|
|
|
|
|
Matchers only end up in one bucket, so once we have
|
|
|
|
removed one, we are done.
|
|
|
|
"""
|
|
|
|
if ADDRESS in matcher:
|
|
|
|
self.address[matcher[ADDRESS]].remove(matcher)
|
|
|
|
return
|
|
|
|
|
2022-09-18 15:22:54 +00:00
|
|
|
if super().remove(matcher):
|
|
|
|
self.build()
|
|
|
|
return
|
|
|
|
|
|
|
|
if CONNECTABLE in matcher:
|
|
|
|
self.connectable.remove(matcher)
|
|
|
|
return
|
2022-08-27 03:07:51 +00:00
|
|
|
|
|
|
|
def match_callbacks(
|
|
|
|
self, service_info: BluetoothServiceInfoBleak
|
|
|
|
) -> list[BluetoothCallbackMatcherWithCallback]:
|
|
|
|
"""Check for a match."""
|
|
|
|
matches = self.match(service_info)
|
|
|
|
for matcher in self.address.get(service_info.address, []):
|
|
|
|
if ble_device_matches(matcher, service_info):
|
|
|
|
matches.append(matcher)
|
2022-09-18 15:22:54 +00:00
|
|
|
for matcher in self.connectable:
|
|
|
|
if ble_device_matches(matcher, service_info):
|
|
|
|
matches.append(matcher)
|
2022-08-27 03:07:51 +00:00
|
|
|
return matches
|
|
|
|
|
|
|
|
|
|
|
|
def _local_name_to_index_key(local_name: str) -> str:
|
|
|
|
"""Convert a local name to an index.
|
|
|
|
|
|
|
|
We check the local name matchers here and raise a ValueError
|
|
|
|
if they try to setup a matcher that will is overly broad
|
|
|
|
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]
|
|
|
|
if "*" in match_part or "[" in match_part:
|
|
|
|
raise ValueError(
|
|
|
|
"Local name matchers may not have patterns in the first "
|
|
|
|
f"{LOCAL_NAME_MIN_MATCH_LENGTH} characters because they "
|
|
|
|
f"would match too broadly ({local_name})"
|
|
|
|
)
|
|
|
|
return match_part
|
|
|
|
|
|
|
|
|
2022-07-24 21:39:53 +00:00
|
|
|
def ble_device_matches(
|
2022-08-27 03:07:51 +00:00
|
|
|
matcher: BluetoothMatcherOptional,
|
2022-08-22 18:02:26 +00:00
|
|
|
service_info: BluetoothServiceInfoBleak,
|
2022-07-24 21:39:53 +00:00
|
|
|
) -> bool:
|
|
|
|
"""Check if a ble device and advertisement_data matches the matcher."""
|
2022-08-27 13:23:47 +00:00
|
|
|
# Don't check address here since all callers already
|
2022-08-27 03:07:51 +00:00
|
|
|
# check the address and we don't want to double check
|
|
|
|
# since it would result in an unreachable reject case.
|
2022-08-22 18:02:26 +00:00
|
|
|
if matcher.get(CONNECTABLE, True) and not service_info.connectable:
|
|
|
|
return False
|
|
|
|
|
|
|
|
advertisement_data = service_info.advertisement
|
2022-07-24 21:39:53 +00:00
|
|
|
if (
|
|
|
|
service_uuid := matcher.get(SERVICE_UUID)
|
2022-08-27 03:07:51 +00:00
|
|
|
) and service_uuid not in advertisement_data.service_uuids:
|
2022-07-24 21:39:53 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
if (
|
|
|
|
service_data_uuid := matcher.get(SERVICE_DATA_UUID)
|
2022-08-27 03:07:51 +00:00
|
|
|
) and service_data_uuid not in advertisement_data.service_data:
|
2022-07-24 21:39:53 +00:00
|
|
|
return False
|
|
|
|
|
2022-08-27 03:07:51 +00:00
|
|
|
if manfacturer_id := matcher.get(MANUFACTURER_ID):
|
|
|
|
if manfacturer_id not in advertisement_data.manufacturer_data:
|
2022-07-24 21:39:53 +00:00
|
|
|
return False
|
2022-08-27 03:07:51 +00:00
|
|
|
if manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START):
|
|
|
|
manufacturer_data_start_bytes = bytearray(manufacturer_data_start)
|
|
|
|
if not any(
|
|
|
|
manufacturer_data.startswith(manufacturer_data_start_bytes)
|
|
|
|
for manufacturer_data in advertisement_data.manufacturer_data.values()
|
|
|
|
):
|
|
|
|
return False
|
|
|
|
|
|
|
|
if (local_name := matcher.get(LOCAL_NAME)) and (
|
2022-09-06 23:12:32 +00:00
|
|
|
(device_name := advertisement_data.local_name or service_info.device.name)
|
|
|
|
is None
|
2022-08-23 14:35:20 +00:00
|
|
|
or not _memorized_fnmatch(
|
|
|
|
device_name,
|
|
|
|
local_name,
|
|
|
|
)
|
|
|
|
):
|
|
|
|
return False
|
|
|
|
|
2022-07-24 21:39:53 +00:00
|
|
|
return True
|
2022-08-23 14:35:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=4096, typed=True)
|
|
|
|
def _compile_fnmatch(pattern: str) -> re.Pattern:
|
|
|
|
"""Compile a fnmatch pattern."""
|
|
|
|
return re.compile(translate(pattern))
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=1024, typed=True)
|
|
|
|
def _memorized_fnmatch(name: str, pattern: str) -> bool:
|
|
|
|
"""Memorized version of fnmatch that has a larger lru_cache.
|
|
|
|
|
|
|
|
The default version of fnmatch only has a lru_cache of 256 entries.
|
|
|
|
With many devices we quickly reach that limit and end up compiling
|
|
|
|
the same pattern over and over again.
|
|
|
|
|
|
|
|
Bluetooth has its own memorized fnmatch with its own lru_cache
|
|
|
|
since the data is going to be relatively the same
|
|
|
|
since the devices will not change frequently.
|
|
|
|
"""
|
|
|
|
return bool(_compile_fnmatch(pattern).match(name))
|