1 """Central manager for tracking devices with random but resolvable MAC addresses."""
3 from __future__
import annotations
5 from collections.abc
import Callable
7 from typing
import cast
9 from bluetooth_data_tools
import get_cipher_for_irk, resolve_private_address
10 from cryptography.hazmat.primitives.ciphers
import Cipher
16 from .const
import DOMAIN
18 _LOGGER = logging.getLogger(__name__)
20 type UnavailableCallback = Callable[[bluetooth.BluetoothServiceInfoBleak],
None]
21 type Cancellable = Callable[[],
None]
25 hass: HomeAssistant, irk: bytes
26 ) -> bluetooth.BluetoothServiceInfoBleak |
None:
27 """Find a BluetoothServiceInfoBleak for the irk.
29 This iterates over all currently visible mac addresses and checks them against `irk`.
30 It returns the newest.
36 cur: bluetooth.BluetoothServiceInfoBleak |
None =
None
37 cipher = get_cipher_for_irk(irk)
39 for service_info
in bluetooth.async_discovered_service_info(hass,
False):
40 if resolve_private_address(cipher, service_info.address):
41 if not cur
or cur.time < service_info.time:
48 """Monitor private bluetooth devices and correlate them with known IRK.
50 This class should not be instanced directly - use `async_get_coordinator` to get an instance.
52 There is a single shared coordinator for all instances of this integration. This is to avoid
53 unnecessary hashing (AES) operations as much as possible.
56 def __init__(self, hass: HomeAssistant) ->
None:
57 """Initialize the manager."""
60 self._irks: dict[bytes, Cipher] = {}
61 self._unavailable_callbacks: dict[bytes, list[UnavailableCallback]] = {}
62 self._service_info_callbacks: dict[
63 bytes, list[bluetooth.BluetoothCallback]
66 self._mac_to_irk: dict[str, bytes] = {}
67 self._irk_to_mac: dict[bytes, str] = {}
71 self._ignored: dict[str, Cancellable] = {}
73 self._unavailability_trackers: dict[bytes, Cancellable] = {}
82 bluetooth.BluetoothScanningMode.ACTIVE,
90 for cancel
in self._ignored.values():
95 self, service_info: bluetooth.BluetoothServiceInfoBleak
98 if resolved := self._mac_to_irk.
get(service_info.address):
99 if callbacks := self._unavailable_callbacks.
get(resolved):
105 if previous_mac := self._irk_to_mac.
get(irk):
106 previous_interval = bluetooth.async_get_learned_advertising_interval(
107 self.
hasshass, previous_mac
108 )
or bluetooth.async_get_fallback_availability_interval(
109 self.
hasshass, previous_mac
111 if previous_interval:
112 bluetooth.async_set_fallback_availability_interval(
113 self.
hasshass, mac, previous_interval
116 self._mac_to_irk.pop(previous_mac,
None)
118 self._mac_to_irk[mac] = irk
119 self._irk_to_mac[irk] = mac
122 self._ignored.pop(mac,
None)
125 if cancel := self._unavailability_trackers.pop(irk,
None):
129 self._unavailability_trackers[irk] = bluetooth.async_track_unavailable(
135 service_info: bluetooth.BluetoothServiceInfoBleak,
136 change: bluetooth.BluetoothChange,
138 mac = service_info.address
140 if mac
in self._ignored:
143 if resolved := self._mac_to_irk.
get(mac):
144 if callbacks := self._service_info_callbacks.
get(resolved):
146 cb(service_info, change)
149 for irk, cipher
in self._irks.items():
150 if resolve_private_address(cipher, service_info.address):
152 if callbacks := self._service_info_callbacks.
get(irk):
154 cb(service_info, change)
157 def _unignore(service_info: bluetooth.BluetoothServiceInfoBleak) ->
None:
158 self._ignored.pop(service_info.address,
None)
160 self._ignored[mac] = bluetooth.async_track_unavailable(
161 self.
hasshass, _unignore, mac,
False
165 """Add irk to list of irks that we can use to resolve RPAs."""
166 if irk
not in self._irks:
169 self._irks[irk] = get_cipher_for_irk(irk)
172 """If no downstream caller is tracking this irk, lets forget it."""
173 if irk
in self._service_info_callbacks
or irk
in self._unavailable_callbacks:
178 if cancel := self._unavailability_trackers.pop(irk,
None):
183 if mac := self._irk_to_mac.pop(irk,
None):
184 self._mac_to_irk.pop(mac,
None)
186 if not self._mac_to_irk:
190 self, callback: bluetooth.BluetoothCallback, irk: bytes
192 """Receive a callback when a new advertisement is received for an irk.
194 Returns a callback that can be used to cancel the registration.
199 callbacks = self._service_info_callbacks.setdefault(irk, [])
200 callbacks.append(callback)
202 def _unsubscribe() -> None:
203 callbacks.remove(callback)
205 self._service_info_callbacks.pop(irk,
None)
212 callback: UnavailableCallback,
215 """Register to receive a callback when an irk is unavailable.
217 Returns a callback that can be used to cancel the registration.
222 callbacks = self._unavailable_callbacks.setdefault(irk, [])
223 callbacks.append(callback)
225 def _unsubscribe() -> None:
226 callbacks.remove(callback)
228 self._unavailable_callbacks.pop(irk,
None)
236 """Create or return an existing PrivateDeviceManager.
238 There should only be one per HomeAssistant instance. Associating private
239 mac addresses with an IRK involves AES operations. We don't want to
242 if existing := hass.data.get(DOMAIN):
243 return cast(PrivateDevicesCoordinator, existing)
None _async_ensure_stopped(self)
None _async_ensure_started(self)
None _async_track_service_info(self, bluetooth.BluetoothServiceInfoBleak service_info, bluetooth.BluetoothChange change)
Cancellable async_track_service_info(self, bluetooth.BluetoothCallback callback, bytes irk)
None __init__(self, HomeAssistant hass)
Cancellable async_track_unavailable(self, UnavailableCallback callback, bytes irk)
None _async_maybe_learn_irk(self, bytes irk)
None _async_irk_resolved_to_mac(self, bytes irk, str mac)
None _async_track_unavailable(self, bluetooth.BluetoothServiceInfoBleak service_info)
None _async_maybe_forget_irk(self, bytes irk)
web.Response get(self, web.Request request, str config_key)
PrivateDevicesCoordinator async_get_coordinator(HomeAssistant hass)
bluetooth.BluetoothServiceInfoBleak|None async_last_service_info(HomeAssistant hass, bytes irk)