Preserve private ble device broadcast interval when MAC address rotates (#100796)

This commit is contained in:
Jc2k 2023-09-24 19:24:12 +01:00 committed by GitHub
parent 49715f300a
commit 0c89f5953f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 6 deletions

View file

@ -102,6 +102,16 @@ class PrivateDevicesCoordinator:
def _async_irk_resolved_to_mac(self, irk: bytes, mac: str) -> None:
if previous_mac := self._irk_to_mac.get(irk):
previous_interval = bluetooth.async_get_learned_advertising_interval(
self.hass, previous_mac
) or bluetooth.async_get_fallback_availability_interval(
self.hass, previous_mac
)
if previous_interval:
bluetooth.async_set_fallback_availability_interval(
self.hass, mac, previous_interval
)
self._mac_to_irk.pop(previous_mac, None)
self._mac_to_irk[mac] = irk

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfLength,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -29,7 +30,9 @@ from .entity import BasePrivateDeviceEntity
class PrivateDeviceSensorEntityDescriptionRequired:
"""Required domain specific fields for sensor entity."""
value_fn: Callable[[bluetooth.BluetoothServiceInfoBleak], str | int | float | None]
value_fn: Callable[
[HomeAssistant, bluetooth.BluetoothServiceInfoBleak], str | int | float | None
]
@dataclass
@ -46,7 +49,7 @@ SENSOR_DESCRIPTIONS = (
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda service_info: service_info.advertisement.rssi,
value_fn=lambda _, service_info: service_info.advertisement.rssi,
state_class=SensorStateClass.MEASUREMENT,
),
PrivateDeviceSensorEntityDescription(
@ -56,7 +59,7 @@ SENSOR_DESCRIPTIONS = (
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda service_info: service_info.advertisement.tx_power,
value_fn=lambda _, service_info: service_info.advertisement.tx_power,
state_class=SensorStateClass.MEASUREMENT,
),
PrivateDeviceSensorEntityDescription(
@ -64,7 +67,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="estimated_distance",
icon="mdi:signal-distance-variant",
native_unit_of_measurement=UnitOfLength.METERS,
value_fn=lambda service_info: service_info.advertisement
value_fn=lambda _, service_info: service_info.advertisement
and service_info.advertisement.tx_power
and calculate_distance_meters(
service_info.advertisement.tx_power * 10, service_info.advertisement.rssi
@ -73,6 +76,22 @@ SENSOR_DESCRIPTIONS = (
device_class=SensorDeviceClass.DISTANCE,
suggested_display_precision=1,
),
PrivateDeviceSensorEntityDescription(
key="estimated_broadcast_interval",
translation_key="estimated_broadcast_interval",
icon="mdi:timer-sync-outline",
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda hass, service_info: bluetooth.async_get_learned_advertising_interval(
hass, service_info.address
)
or bluetooth.async_get_fallback_availability_interval(
hass, service_info.address
)
or bluetooth.FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
suggested_display_precision=1,
),
)
@ -124,4 +143,4 @@ class PrivateBLEDeviceSensor(BasePrivateDeviceEntity, SensorEntity):
def native_value(self) -> str | int | float | None:
"""Return the state of the sensor."""
assert self._last_info
return self.entity_description.value_fn(self._last_info)
return self.entity_description.value_fn(self.hass, self._last_info)

View file

@ -24,6 +24,9 @@
},
"estimated_distance": {
"name": "Estimated distance"
},
"estimated_broadcast_interval": {
"name": "Estimated broadcast interval"
}
}
}

View file

@ -6,6 +6,9 @@ import time
from homeassistant.components.bluetooth.advertisement_tracker import (
ADVERTISING_TIMES_NEEDED,
)
from homeassistant.components.bluetooth.api import (
async_get_fallback_availability_interval,
)
from homeassistant.core import HomeAssistant
from . import (
@ -181,3 +184,23 @@ async def test_old_tracker_leave_home(
state = hass.states.get("device_tracker.private_ble_device_000000")
assert state
assert state.state == "not_home"
async def test_mac_rotation(
hass: HomeAssistant,
enable_bluetooth: None,
entity_registry_enabled_by_default: None,
) -> None:
"""Test sensors get value when we receive a broadcast."""
await async_mock_config_entry(hass)
assert async_get_fallback_availability_interval(hass, MAC_RPA_VALID_1) is None
assert async_get_fallback_availability_interval(hass, MAC_RPA_VALID_2) is None
for i in range(ADVERTISING_TIMES_NEEDED):
await async_inject_broadcast(
hass, MAC_RPA_VALID_1, mfr_data=bytes(i), broadcast_time=i * 10
)
await async_inject_broadcast(hass, MAC_RPA_VALID_2)
assert async_get_fallback_availability_interval(hass, MAC_RPA_VALID_2) == 10

View file

@ -1,9 +1,18 @@
"""Tests for sensors."""
from homeassistant.components.bluetooth import async_set_fallback_availability_interval
from homeassistant.components.bluetooth.advertisement_tracker import (
ADVERTISING_TIMES_NEEDED,
)
from homeassistant.core import HomeAssistant
from . import MAC_RPA_VALID_1, async_inject_broadcast, async_mock_config_entry
from . import (
MAC_RPA_VALID_1,
MAC_RPA_VALID_2,
async_inject_broadcast,
async_mock_config_entry,
)
async def test_sensor_unavailable(
@ -45,3 +54,55 @@ async def test_sensors_come_home(
state = hass.states.get("sensor.private_ble_device_000000_signal_strength")
assert state
assert state.state == "-63"
async def test_estimated_broadcast_interval(
hass: HomeAssistant,
enable_bluetooth: None,
entity_registry_enabled_by_default: None,
) -> None:
"""Test sensors get value when we receive a broadcast."""
await async_mock_config_entry(hass)
await async_inject_broadcast(hass, MAC_RPA_VALID_1)
# With no fallback and no learned interval, we should use the global default
state = hass.states.get(
"sensor.private_ble_device_000000_estimated_broadcast_interval"
)
assert state
assert state.state == "900"
# Fallback interval trumps const default
async_set_fallback_availability_interval(hass, MAC_RPA_VALID_1, 90)
await async_inject_broadcast(hass, MAC_RPA_VALID_1.upper())
state = hass.states.get(
"sensor.private_ble_device_000000_estimated_broadcast_interval"
)
assert state
assert state.state == "90"
# Learned broadcast interval takes over from fallback interval
for i in range(ADVERTISING_TIMES_NEEDED):
await async_inject_broadcast(
hass, MAC_RPA_VALID_1, mfr_data=bytes(i), broadcast_time=i * 10
)
state = hass.states.get(
"sensor.private_ble_device_000000_estimated_broadcast_interval"
)
assert state
assert state.state == "10"
# MAC address changes, the broadcast interval is kept
await async_inject_broadcast(hass, MAC_RPA_VALID_2.upper())
state = hass.states.get(
"sensor.private_ble_device_000000_estimated_broadcast_interval"
)
assert state
assert state.state == "10"