Skip to content

Commit ea0d2fc

Browse files
authored
feat: refactor scanner history to live on the scanner itself (#227)
1 parent 3f2adc3 commit ea0d2fc

File tree

8 files changed

+169
-201
lines changed

8 files changed

+169
-201
lines changed

src/habluetooth/base_scanner.pxd

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,37 @@ cdef class BaseHaScanner:
2828
cdef public object details
2929
cdef public object current_mode
3030
cdef public object requested_mode
31+
cdef public dict _connect_failures
32+
cdef public dict _connect_in_progress
33+
34+
cpdef void _clear_connection_history(self) except *
35+
36+
cpdef void _finished_connecting(self, str address, bint connected) except *
37+
38+
cdef void _increase_count(self, dict target, str address) except *
39+
40+
cdef void _add_connect_failure(self, str address) except *
41+
42+
cpdef void _add_connecting(self, str address) except *
43+
44+
cdef void _remove_connecting(self, str address) except *
45+
46+
cdef void _clear_connect_failure(self, str address) except *
47+
48+
@cython.locals(
49+
in_progress=Py_ssize_t,
50+
count=Py_ssize_t
51+
)
52+
cpdef _connections_in_progress(self)
53+
54+
cpdef _connection_failures(self, str address)
55+
56+
@cython.locals(
57+
score=double,
58+
scanner_connections_in_progress=Py_ssize_t,
59+
previous_failures=Py_ssize_t
60+
)
61+
cpdef _score_connection_paths(self, int rssi_diff, object scanner_device)
3162

3263
cpdef tuple get_discovered_device_advertisement_data(self, str address)
3364

src/habluetooth/base_scanner.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from bleak.backends.device import BLEDevice
1313
from bleak.backends.scanner import AdvertisementData
14+
from bleak_retry_connector import NO_RSSI_VALUE
1415
from bluetooth_adapters import adapter_human_name
1516
from bluetooth_data_tools import monotonic_time_coarse, parse_advertisement_data_bytes
1617

@@ -27,6 +28,7 @@
2728
HaBluetoothConnector,
2829
HaScannerDetails,
2930
)
31+
from .scanner_device import BluetoothScannerDevice
3032
from .storage import DiscoveredDeviceAdvertisementData
3133

3234
SCANNER_WATCHDOG_INTERVAL_SECONDS: Final = SCANNER_WATCHDOG_INTERVAL.total_seconds()
@@ -44,6 +46,9 @@ class BaseHaScanner:
4446

4547
__slots__ = (
4648
"_cancel_watchdog",
49+
"_connect_failures",
50+
"_connect_in_progress",
51+
"_connecting",
4752
"_connecting",
4853
"_last_detection",
4954
"_loop",
@@ -90,6 +95,78 @@ def __init__(
9095
name=self.name,
9196
adapter=self.adapter,
9297
)
98+
self._connect_failures: dict[str, int] = {}
99+
self._connect_in_progress: dict[str, int] = {}
100+
101+
def _clear_connection_history(self) -> None:
102+
"""Clear the connection history for a scanner."""
103+
self._connect_failures.clear()
104+
self._connect_in_progress.clear()
105+
106+
def _finished_connecting(self, address: str, connected: bool) -> None:
107+
"""Finished connecting."""
108+
self._remove_connecting(address)
109+
if connected:
110+
self._clear_connect_failure(address)
111+
else:
112+
self._add_connect_failure(address)
113+
114+
def _increase_count(self, target: dict[str, int], address: str) -> None:
115+
"""Increase the reference count."""
116+
if address in target:
117+
target[address] += 1
118+
else:
119+
target[address] = 1
120+
121+
def _add_connect_failure(self, address: str) -> None:
122+
"""Add a connect failure."""
123+
self._increase_count(self._connect_failures, address)
124+
125+
def _add_connecting(self, address: str) -> None:
126+
"""Add a connecting."""
127+
self._increase_count(self._connect_in_progress, address)
128+
129+
def _remove_connecting(self, address: str) -> None:
130+
"""Remove a connecting."""
131+
if address not in self._connect_in_progress:
132+
_LOGGER.warning(
133+
"Removing a non-existing connecting %s %s", self.name, address
134+
)
135+
return
136+
self._connect_in_progress[address] -= 1
137+
if not self._connect_in_progress[address]:
138+
del self._connect_in_progress[address]
139+
140+
def _clear_connect_failure(self, address: str) -> None:
141+
"""Clear a connect failure."""
142+
self._connect_failures.pop(address, None)
143+
144+
def _score_connection_paths(
145+
self, rssi_diff: _int, scanner_device: BluetoothScannerDevice
146+
) -> float:
147+
"""Score the connection paths."""
148+
address = scanner_device.ble_device.address
149+
score = scanner_device.advertisement.rssi or NO_RSSI_VALUE
150+
scanner_connections_in_progress = len(self._connect_in_progress)
151+
previous_failures = self._connect_failures.get(address, 0)
152+
if scanner_connections_in_progress:
153+
# Very large penalty for multiple connections in progress
154+
# to avoid overloading the adapter
155+
score -= rssi_diff * scanner_connections_in_progress * 1.01
156+
if previous_failures:
157+
score -= rssi_diff * previous_failures * 0.51
158+
return score
159+
160+
def _connections_in_progress(self) -> int:
161+
"""Return if the connection is in progress."""
162+
in_progress = 0
163+
for count in self._connect_in_progress.values():
164+
in_progress += count
165+
return in_progress
166+
167+
def _connection_failures(self, address: str) -> int:
168+
"""Return the number of failures."""
169+
return self._connect_failures.get(address, 0)
93170

94171
def time_since_last_detection(self) -> float:
95172
"""Return the time since the last detection."""

src/habluetooth/manager.pxd

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,38 +34,6 @@ cdef class BleakCallback:
3434
cdef public object callback
3535
cdef public dict filters
3636

37-
cdef class ConnectionHistory:
38-
39-
cdef public dict _failures
40-
cdef public dict _connecting
41-
42-
cpdef void clear(self, BaseHaScanner scanner) except *
43-
44-
cpdef void finished_connecting(self, BaseHaScanner scanner, str address, bint connected) except *
45-
46-
cdef void _increase_ref_count(self, dict target, str address) except *
47-
48-
cdef void _add_connect_failure(self, BaseHaScanner scanner, str address) except *
49-
50-
cpdef void add_connecting(self, BaseHaScanner scanner, str address) except *
51-
52-
cdef void _remove_connecting(self, BaseHaScanner scanner, str address) except *
53-
54-
cdef void _clear_connect_failure(self, BaseHaScanner scanner, str address) except *
55-
56-
@cython.locals(
57-
in_progress=Py_ssize_t
58-
)
59-
cpdef in_progress(self, BaseHaScanner scanner)
60-
61-
cpdef failures(self, BaseHaScanner scanner, str address)
62-
63-
@cython.locals(
64-
score=double,
65-
scanner_connections_in_progress=Py_ssize_t,
66-
previous_failures=Py_ssize_t
67-
)
68-
cpdef score_connection_paths(self, int rssi_diff, object scanner_device)
6937

7038
cdef class BluetoothManager:
7139

@@ -84,7 +52,6 @@ cdef class BluetoothManager:
8452
cdef public dict _sources
8553
cdef public object _bluetooth_adapters
8654
cdef public object slot_manager
87-
cdef public ConnectionHistory _connection_history
8855
cdef public bint _debug
8956
cdef public bint shutdown
9057
cdef public object _loop

src/habluetooth/manager.py

Lines changed: 2 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -100,100 +100,6 @@ def __init__(
100100
self.filters = filters
101101

102102

103-
class ConnectionHistory:
104-
"""Connection history."""
105-
106-
__slots__ = ("_connecting", "_failures")
107-
108-
def __init__(self) -> None:
109-
"""Init connection history."""
110-
self._failures: dict[BaseHaScanner, dict[str, int]] = {}
111-
self._connecting: dict[BaseHaScanner, dict[str, int]] = {}
112-
113-
def clear(self, scanner: BaseHaScanner) -> None:
114-
"""Clear the connection history for a scanner."""
115-
self._failures.pop(scanner, None)
116-
self._connecting.pop(scanner, None)
117-
118-
def finished_connecting(
119-
self, scanner: BaseHaScanner, address: str, connected: bool
120-
) -> None:
121-
"""Finished connecting."""
122-
self._remove_connecting(scanner, address)
123-
if connected:
124-
self._clear_connect_failure(scanner, address)
125-
else:
126-
self._add_connect_failure(scanner, address)
127-
128-
def _increase_ref_count(self, target: dict[str, int], address: str) -> None:
129-
"""Increase the reference count."""
130-
if address in target:
131-
target[address] += 1
132-
else:
133-
target[address] = 1
134-
135-
def _add_connect_failure(self, scanner: BaseHaScanner, address: str) -> None:
136-
"""Add a connect failure."""
137-
self._increase_ref_count(self._failures.setdefault(scanner, {}), address)
138-
139-
def add_connecting(self, scanner: BaseHaScanner, address: str) -> None:
140-
"""Add a connecting."""
141-
self._increase_ref_count(self._connecting.setdefault(scanner, {}), address)
142-
143-
def _remove_connecting(self, scanner: BaseHaScanner, address: str) -> None:
144-
"""Remove a connecting."""
145-
if scanner not in self._connecting or address not in self._connecting[scanner]:
146-
_LOGGER.warning(
147-
"Removing a non-existing connecting %s %s", scanner, address
148-
)
149-
return
150-
self._connecting[scanner][address] -= 1
151-
if not self._connecting[scanner][address]:
152-
del self._connecting[scanner][address]
153-
if not self._connecting[scanner]:
154-
del self._connecting[scanner]
155-
156-
def _clear_connect_failure(self, scanner: BaseHaScanner, address: str) -> None:
157-
"""Clear a connect failure."""
158-
if scanner not in self._failures or address not in self._failures[scanner]:
159-
return
160-
del self._failures[scanner][address]
161-
if not self._failures[scanner]:
162-
del self._failures[scanner]
163-
164-
def score_connection_paths(
165-
self, rssi_diff: _int, scanner_device: BluetoothScannerDevice
166-
) -> float:
167-
"""Score the connection paths."""
168-
scanner = scanner_device.scanner
169-
address = scanner_device.ble_device.address
170-
score = scanner_device.advertisement.rssi or NO_RSSI_VALUE
171-
scanner_connections_in_progress = len(self._connecting.get(scanner, ()))
172-
previous_failures = self._failures.get(scanner, {}).get(address, 0)
173-
if scanner_connections_in_progress:
174-
# Very large penalty for multiple connections in progress
175-
# to avoid overloading the adapter
176-
score -= rssi_diff * scanner_connections_in_progress * 1.01
177-
if previous_failures:
178-
score -= rssi_diff * previous_failures * 0.51
179-
return score
180-
181-
def in_progress(self, scanner: BaseHaScanner) -> int:
182-
"""Return if the connection is in progress."""
183-
if scanner not in self._connecting:
184-
return 0
185-
in_progress = 0
186-
for count in self._connecting[scanner].values():
187-
in_progress += count
188-
return in_progress
189-
190-
def failures(self, scanner: BaseHaScanner, address: str) -> int:
191-
"""Return the number of failures."""
192-
if scanner not in self._failures or address not in self._failures[scanner]:
193-
return 0
194-
return self._failures[scanner][address]
195-
196-
197103
class BluetoothManager:
198104
"""Manage Bluetooth."""
199105

@@ -258,7 +164,6 @@ def __init__(
258164
self._sources: dict[str, BaseHaScanner] = {}
259165
self._bluetooth_adapters = bluetooth_adapters or get_adapters()
260166
self.slot_manager = slot_manager or BleakSlotManager()
261-
self._connection_history = ConnectionHistory()
262167
self._cancel_allocation_callbacks = (
263168
self.slot_manager.register_allocation_callback(
264169
self._async_slot_manager_changed
@@ -843,7 +748,7 @@ def _async_unregister_scanner_internal(
843748
_LOGGER.debug("Unregistering scanner %s", scanner.name)
844749
self._advertisement_tracker.async_remove_source(scanner.source)
845750
scanners.remove(scanner)
846-
self._connection_history.clear(scanner)
751+
scanner._clear_connection_history()
847752
del self._sources[scanner.source]
848753
del self._adapter_sources[scanner.adapter]
849754
self._allocations.pop(scanner.source, None)
@@ -866,7 +771,7 @@ def async_register_scanner(
866771
source=scanner.source, slots=0, free=0, allocated=[]
867772
)
868773
scanners.add(scanner)
869-
self._connection_history.clear(scanner)
774+
scanner._clear_connection_history()
870775
self._sources[scanner.source] = scanner
871776
self._adapter_sources[scanner.adapter] = scanner.source
872777
if connection_slots:

src/habluetooth/scanner_device.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ class BluetoothScannerDevice:
1919
scanner: BaseHaScanner
2020
ble_device: BLEDevice
2121
advertisement: AdvertisementData
22+
23+
def score_connection_path(self, rssi_diff: int) -> float:
24+
"""Return a score for the connection path to this device."""
25+
return self.scanner._score_connection_paths(rssi_diff, self)

0 commit comments

Comments
 (0)