mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
Preserve private ble device broadcast interval when MAC address rotates (#100796)
This commit is contained in:
parent
49715f300a
commit
0c89f5953f
@ -102,6 +102,16 @@ class PrivateDevicesCoordinator:
|
|||||||
|
|
||||||
def _async_irk_resolved_to_mac(self, irk: bytes, mac: str) -> None:
|
def _async_irk_resolved_to_mac(self, irk: bytes, mac: str) -> None:
|
||||||
if previous_mac := self._irk_to_mac.get(irk):
|
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.pop(previous_mac, None)
|
||||||
|
|
||||||
self._mac_to_irk[mac] = irk
|
self._mac_to_irk[mac] = irk
|
||||||
|
@ -18,6 +18,7 @@ from homeassistant.const import (
|
|||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
|
UnitOfTime,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@ -29,7 +30,9 @@ from .entity import BasePrivateDeviceEntity
|
|||||||
class PrivateDeviceSensorEntityDescriptionRequired:
|
class PrivateDeviceSensorEntityDescriptionRequired:
|
||||||
"""Required domain specific fields for sensor entity."""
|
"""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
|
@dataclass
|
||||||
@ -46,7 +49,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
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,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
PrivateDeviceSensorEntityDescription(
|
PrivateDeviceSensorEntityDescription(
|
||||||
@ -56,7 +59,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
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,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
PrivateDeviceSensorEntityDescription(
|
PrivateDeviceSensorEntityDescription(
|
||||||
@ -64,7 +67,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
translation_key="estimated_distance",
|
translation_key="estimated_distance",
|
||||||
icon="mdi:signal-distance-variant",
|
icon="mdi:signal-distance-variant",
|
||||||
native_unit_of_measurement=UnitOfLength.METERS,
|
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 service_info.advertisement.tx_power
|
||||||
and calculate_distance_meters(
|
and calculate_distance_meters(
|
||||||
service_info.advertisement.tx_power * 10, service_info.advertisement.rssi
|
service_info.advertisement.tx_power * 10, service_info.advertisement.rssi
|
||||||
@ -73,6 +76,22 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
device_class=SensorDeviceClass.DISTANCE,
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
suggested_display_precision=1,
|
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:
|
def native_value(self) -> str | int | float | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
assert self._last_info
|
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": {
|
"estimated_distance": {
|
||||||
"name": "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 (
|
from homeassistant.components.bluetooth.advertisement_tracker import (
|
||||||
ADVERTISING_TIMES_NEEDED,
|
ADVERTISING_TIMES_NEEDED,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.bluetooth.api import (
|
||||||
|
async_get_fallback_availability_interval,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
@ -181,3 +184,23 @@ async def test_old_tracker_leave_home(
|
|||||||
state = hass.states.get("device_tracker.private_ble_device_000000")
|
state = hass.states.get("device_tracker.private_ble_device_000000")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "not_home"
|
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."""
|
"""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 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(
|
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")
|
state = hass.states.get("sensor.private_ble_device_000000_signal_strength")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "-63"
|
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…
x
Reference in New Issue
Block a user