Preserve private ble device broadcast interval when MAC address rotates (#100796)
This commit is contained in:
parent
49715f300a
commit
0c89f5953f
5 changed files with 122 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
},
|
||||
"estimated_distance": {
|
||||
"name": "Estimated distance"
|
||||
},
|
||||
"estimated_broadcast_interval": {
|
||||
"name": "Estimated broadcast interval"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue