Skip to content

Commit 1c8db59

Browse files
authored
fix: incorrect advertising interval calculation when scanner pauses for connections (#283)
1 parent bed65b1 commit 1c8db59

File tree

5 files changed

+104
-2
lines changed

5 files changed

+104
-2
lines changed

src/habluetooth/advertisement_tracker.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ cdef class AdvertisementTracker:
1515
cpdef void async_collect(self, BluetoothServiceInfoBleak service_info)
1616

1717
cpdef void async_remove_address(self, object address)
18+
19+
cpdef void async_scanner_paused(self, str source)

src/habluetooth/advertisement_tracker.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,18 @@ def async_remove_source(self, source: str) -> None:
8080
for address, tracked_source in list(self.sources.items()):
8181
if tracked_source == source:
8282
self.async_remove_address(address)
83+
84+
def async_scanner_paused(self, source: str) -> None:
85+
"""
86+
Clear timing collection data when scanner is paused.
87+
88+
When a scanner pauses to establish a connection, it stops listening
89+
for advertisements. If we don't clear the timing data, the next
90+
advertisement after the connection attempt will create an incorrectly
91+
large interval measurement (time_after_connection - time_before_connection)
92+
which doesn't represent the actual advertising interval of the device.
93+
"""
94+
# Only iterate through timing data (typically much smaller than sources)
95+
for address in list(self._timings):
96+
if self.sources.get(address) == source:
97+
del self._timings[address]

src/habluetooth/base_scanner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ def _add_connect_failure(self, address: str) -> None:
134134
def _add_connecting(self, address: str) -> None:
135135
"""Add a connecting."""
136136
self._increase_count(self._connect_in_progress, address)
137+
# Clear timing collection data when scanner pauses for connection
138+
# to prevent collecting invalid advertising interval data
139+
self._manager._advertisement_tracker.async_scanner_paused(self.source)
137140

138141
def _remove_connecting(self, address: str) -> None:
139142
"""Remove a connecting."""
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Test that advertising interval tracking is properly cleared when scanner pauses."""
2+
3+
import pytest
4+
5+
from habluetooth.advertisement_tracker import AdvertisementTracker
6+
from habluetooth.base_scanner import BaseHaScanner
7+
from habluetooth.central_manager import get_manager
8+
9+
10+
@pytest.mark.asyncio
11+
async def test_scanner_paused_clears_timing_data():
12+
"""Test timing data is cleared when scanner pauses but intervals are preserved."""
13+
tracker = AdvertisementTracker()
14+
source = "test_scanner"
15+
address = "AA:BB:CC:DD:EE:FF"
16+
17+
# Simulate collecting timing data
18+
tracker.sources[address] = source
19+
tracker._timings[address] = [1.0, 2.0, 3.0] # Some timing data
20+
tracker.intervals[address] = 10.0 # Already learned interval
21+
22+
# Call async_scanner_paused
23+
tracker.async_scanner_paused(source)
24+
25+
# Check that timing data is cleared but interval is preserved
26+
assert address not in tracker._timings
27+
assert tracker.intervals[address] == 10.0 # Interval should still be there
28+
assert tracker.sources[address] == source # Source mapping should still be there
29+
30+
31+
@pytest.mark.asyncio
32+
async def test_scanner_paused_only_affects_matching_source():
33+
"""Test that pausing only affects devices from the matching source."""
34+
tracker = AdvertisementTracker()
35+
source1 = "scanner1"
36+
source2 = "scanner2"
37+
address1 = "AA:BB:CC:DD:EE:01"
38+
address2 = "AA:BB:CC:DD:EE:02"
39+
40+
# Set up data for two sources
41+
tracker.sources[address1] = source1
42+
tracker.sources[address2] = source2
43+
tracker._timings[address1] = [1.0, 2.0]
44+
tracker._timings[address2] = [1.0, 2.0]
45+
tracker.intervals[address1] = 5.0
46+
tracker.intervals[address2] = 6.0
47+
48+
# Pause only source1
49+
tracker.async_scanner_paused(source1)
50+
51+
# Check that only source1 timing is cleared
52+
assert address1 not in tracker._timings
53+
assert address2 in tracker._timings # source2 should still have timing data
54+
assert tracker.intervals[address1] == 5.0 # Intervals preserved
55+
assert tracker.intervals[address2] == 6.0
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_connection_clears_timing_data():
60+
"""Test that timing data is cleared when a connection is initiated."""
61+
# Get the manager that was set up by the fixture
62+
test_manager = get_manager()
63+
64+
# Create actual BaseHaScanner to test the method
65+
real_scanner = BaseHaScanner(
66+
source="test_scanner", adapter="hci0", connectable=True
67+
)
68+
# BaseHaScanner gets the manager internally via get_manager()
69+
70+
# Set up some timing data
71+
address = "AA:BB:CC:DD:EE:FF"
72+
test_manager._advertisement_tracker.sources[address] = real_scanner.source
73+
test_manager._advertisement_tracker._timings[address] = [1.0, 2.0, 3.0]
74+
test_manager._advertisement_tracker.intervals[address] = 10.0
75+
76+
# Call _add_connecting which should clear timing data
77+
real_scanner._add_connecting(address)
78+
79+
# Verify timing data was cleared but interval preserved
80+
assert address not in test_manager._advertisement_tracker._timings
81+
assert test_manager._advertisement_tracker.intervals.get(address) == 10.0
82+
assert address in real_scanner._connect_in_progress

tests/test_scanner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
set_manager,
2525
)
2626
from habluetooth.channels.bluez import (
27-
ADV_MONITOR_DEVICE_FOUND,
28-
DEVICE_FOUND,
2927
BluetoothMGMTProtocol,
3028
MGMTBluetoothCtl,
3129
)
@@ -44,6 +42,8 @@
4442
)
4543
from .conftest import FakeBluetoothAdapters
4644

45+
DEVICE_FOUND = 0x0012
46+
ADV_MONITOR_DEVICE_FOUND = 0x002F
4747
IS_WINDOWS = 'os.name == "nt"'
4848
IS_POSIX = 'os.name == "posix"'
4949
NOT_POSIX = 'os.name != "posix"'

0 commit comments

Comments
 (0)