mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Handle iBeacons that broadcast multiple different uuids (#79011)
* Handle iBeacons that broadcast multiple different uuids * fix flip-flopping between uuids * naming
This commit is contained in:
parent
fc58d88770
commit
02731efc4c
@ -54,7 +54,7 @@ def make_short_address(address: str) -> str:
|
|||||||
@callback
|
@callback
|
||||||
def async_name(
|
def async_name(
|
||||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
unique_address: bool = False,
|
unique_address: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Return a name for the device."""
|
"""Return a name for the device."""
|
||||||
@ -62,7 +62,7 @@ def async_name(
|
|||||||
service_info.name,
|
service_info.name,
|
||||||
service_info.name.replace("_", ":"),
|
service_info.name.replace("_", ":"),
|
||||||
):
|
):
|
||||||
base_name = f"{parsed.uuid} {parsed.major}.{parsed.minor}"
|
base_name = f"{ibeacon_advertisement.uuid} {ibeacon_advertisement.major}.{ibeacon_advertisement.minor}"
|
||||||
else:
|
else:
|
||||||
base_name = service_info.name
|
base_name = service_info.name
|
||||||
if unique_address:
|
if unique_address:
|
||||||
@ -77,7 +77,7 @@ def _async_dispatch_update(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
new: bool,
|
new: bool,
|
||||||
unique_address: bool,
|
unique_address: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -87,15 +87,15 @@ def _async_dispatch_update(
|
|||||||
hass,
|
hass,
|
||||||
SIGNAL_IBEACON_DEVICE_NEW,
|
SIGNAL_IBEACON_DEVICE_NEW,
|
||||||
device_id,
|
device_id,
|
||||||
async_name(service_info, parsed, unique_address),
|
async_name(service_info, ibeacon_advertisement, unique_address),
|
||||||
parsed,
|
ibeacon_advertisement,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass,
|
hass,
|
||||||
signal_seen(device_id),
|
signal_seen(device_id),
|
||||||
parsed,
|
ibeacon_advertisement,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -117,7 +117,9 @@ class IBeaconCoordinator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# iBeacons with fixed MAC addresses
|
# iBeacons with fixed MAC addresses
|
||||||
self._last_rssi_by_unique_id: dict[str, int] = {}
|
self._last_ibeacon_advertisement_by_unique_id: dict[
|
||||||
|
str, iBeaconAdvertisement
|
||||||
|
] = {}
|
||||||
self._group_ids_by_address: dict[str, set[str]] = {}
|
self._group_ids_by_address: dict[str, set[str]] = {}
|
||||||
self._unique_ids_by_address: dict[str, set[str]] = {}
|
self._unique_ids_by_address: dict[str, set[str]] = {}
|
||||||
self._unique_ids_by_group_id: dict[str, set[str]] = {}
|
self._unique_ids_by_group_id: dict[str, set[str]] = {}
|
||||||
@ -162,21 +164,23 @@ class IBeaconCoordinator:
|
|||||||
for unique_id in unique_ids:
|
for unique_id in unique_ids:
|
||||||
if device := self._dev_reg.async_get_device({(DOMAIN, unique_id)}):
|
if device := self._dev_reg.async_get_device({(DOMAIN, unique_id)}):
|
||||||
self._dev_reg.async_remove_device(device.id)
|
self._dev_reg.async_remove_device(device.id)
|
||||||
self._last_rssi_by_unique_id.pop(unique_id, None)
|
self._last_ibeacon_advertisement_by_unique_id.pop(unique_id, None)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_convert_random_mac_tracking(
|
def _async_convert_random_mac_tracking(
|
||||||
self,
|
self,
|
||||||
group_id: str,
|
group_id: str,
|
||||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Switch to random mac tracking method when a group is using rotating mac addresses."""
|
"""Switch to random mac tracking method when a group is using rotating mac addresses."""
|
||||||
self._group_ids_random_macs.add(group_id)
|
self._group_ids_random_macs.add(group_id)
|
||||||
self._async_purge_untrackable_entities(self._unique_ids_by_group_id[group_id])
|
self._async_purge_untrackable_entities(self._unique_ids_by_group_id[group_id])
|
||||||
self._unique_ids_by_group_id.pop(group_id)
|
self._unique_ids_by_group_id.pop(group_id)
|
||||||
self._addresses_by_group_id.pop(group_id)
|
self._addresses_by_group_id.pop(group_id)
|
||||||
self._async_update_ibeacon_with_random_mac(group_id, service_info, parsed)
|
self._async_update_ibeacon_with_random_mac(
|
||||||
|
group_id, service_info, ibeacon_advertisement
|
||||||
|
)
|
||||||
|
|
||||||
def _async_track_ibeacon_with_unique_address(
|
def _async_track_ibeacon_with_unique_address(
|
||||||
self, address: str, group_id: str, unique_id: str
|
self, address: str, group_id: str, unique_id: str
|
||||||
@ -197,49 +201,55 @@ class IBeaconCoordinator:
|
|||||||
"""Update from a bluetooth callback."""
|
"""Update from a bluetooth callback."""
|
||||||
if service_info.address in self._ignore_addresses:
|
if service_info.address in self._ignore_addresses:
|
||||||
return
|
return
|
||||||
if not (parsed := parse(service_info)):
|
if not (ibeacon_advertisement := parse(service_info)):
|
||||||
return
|
return
|
||||||
group_id = f"{parsed.uuid}_{parsed.major}_{parsed.minor}"
|
group_id = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}"
|
||||||
|
|
||||||
if group_id in self._group_ids_random_macs:
|
if group_id in self._group_ids_random_macs:
|
||||||
self._async_update_ibeacon_with_random_mac(group_id, service_info, parsed)
|
self._async_update_ibeacon_with_random_mac(
|
||||||
|
group_id, service_info, ibeacon_advertisement
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._async_update_ibeacon_with_unique_address(group_id, service_info, parsed)
|
self._async_update_ibeacon_with_unique_address(
|
||||||
|
group_id, service_info, ibeacon_advertisement
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_ibeacon_with_random_mac(
|
def _async_update_ibeacon_with_random_mac(
|
||||||
self,
|
self,
|
||||||
group_id: str,
|
group_id: str,
|
||||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update iBeacons with random mac addresses."""
|
"""Update iBeacons with random mac addresses."""
|
||||||
new = group_id not in self._last_seen_by_group_id
|
new = group_id not in self._last_seen_by_group_id
|
||||||
self._last_seen_by_group_id[group_id] = service_info
|
self._last_seen_by_group_id[group_id] = service_info
|
||||||
self._unavailable_group_ids.discard(group_id)
|
self._unavailable_group_ids.discard(group_id)
|
||||||
_async_dispatch_update(self.hass, group_id, service_info, parsed, new, False)
|
_async_dispatch_update(
|
||||||
|
self.hass, group_id, service_info, ibeacon_advertisement, new, False
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_ibeacon_with_unique_address(
|
def _async_update_ibeacon_with_unique_address(
|
||||||
self,
|
self,
|
||||||
group_id: str,
|
group_id: str,
|
||||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Handle iBeacon with a fixed mac address
|
# Handle iBeacon with a fixed mac address
|
||||||
# and or detect if the iBeacon is using a rotating mac address
|
# and or detect if the iBeacon is using a rotating mac address
|
||||||
# and switch to random mac tracking method
|
# and switch to random mac tracking method
|
||||||
address = service_info.address
|
address = service_info.address
|
||||||
unique_id = f"{group_id}_{address}"
|
unique_id = f"{group_id}_{address}"
|
||||||
new = unique_id not in self._last_rssi_by_unique_id
|
new = unique_id not in self._last_ibeacon_advertisement_by_unique_id
|
||||||
# Reject creating new trackers if the name is not set
|
# Reject creating new trackers if the name is not set
|
||||||
if new and (
|
if new and (
|
||||||
service_info.device.name is None
|
service_info.device.name is None
|
||||||
or service_info.device.name.replace("-", ":") == service_info.device.address
|
or service_info.device.name.replace("-", ":") == service_info.device.address
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
self._last_rssi_by_unique_id[unique_id] = service_info.rssi
|
self._last_ibeacon_advertisement_by_unique_id[unique_id] = ibeacon_advertisement
|
||||||
self._async_track_ibeacon_with_unique_address(address, group_id, unique_id)
|
self._async_track_ibeacon_with_unique_address(address, group_id, unique_id)
|
||||||
if address not in self._unavailable_trackers:
|
if address not in self._unavailable_trackers:
|
||||||
self._unavailable_trackers[address] = bluetooth.async_track_unavailable(
|
self._unavailable_trackers[address] = bluetooth.async_track_unavailable(
|
||||||
@ -259,10 +269,14 @@ class IBeaconCoordinator:
|
|||||||
# group_id we remove all the trackers for that group_id
|
# group_id we remove all the trackers for that group_id
|
||||||
# as it means the addresses are being rotated.
|
# as it means the addresses are being rotated.
|
||||||
if len(self._addresses_by_group_id[group_id]) >= MAX_IDS:
|
if len(self._addresses_by_group_id[group_id]) >= MAX_IDS:
|
||||||
self._async_convert_random_mac_tracking(group_id, service_info, parsed)
|
self._async_convert_random_mac_tracking(
|
||||||
|
group_id, service_info, ibeacon_advertisement
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
_async_dispatch_update(self.hass, unique_id, service_info, parsed, new, True)
|
_async_dispatch_update(
|
||||||
|
self.hass, unique_id, service_info, ibeacon_advertisement, new, True
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_stop(self) -> None:
|
def _async_stop(self) -> None:
|
||||||
@ -294,21 +308,21 @@ class IBeaconCoordinator:
|
|||||||
here and send them over the dispatcher periodically to
|
here and send them over the dispatcher periodically to
|
||||||
ensure the distance calculation is update.
|
ensure the distance calculation is update.
|
||||||
"""
|
"""
|
||||||
for unique_id, rssi in self._last_rssi_by_unique_id.items():
|
for (
|
||||||
|
unique_id,
|
||||||
|
ibeacon_advertisement,
|
||||||
|
) in self._last_ibeacon_advertisement_by_unique_id.items():
|
||||||
address = unique_id.split("_")[-1]
|
address = unique_id.split("_")[-1]
|
||||||
if (
|
if (
|
||||||
(
|
service_info := bluetooth.async_last_service_info(
|
||||||
service_info := bluetooth.async_last_service_info(
|
self.hass, address, connectable=False
|
||||||
self.hass, address, connectable=False
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
and service_info.rssi != rssi
|
) and service_info.rssi != ibeacon_advertisement.rssi:
|
||||||
and (parsed := parse(service_info))
|
ibeacon_advertisement.update_rssi(service_info.rssi)
|
||||||
):
|
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self.hass,
|
self.hass,
|
||||||
signal_seen(unique_id),
|
signal_seen(unique_id),
|
||||||
parsed,
|
ibeacon_advertisement,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -26,7 +26,7 @@ async def async_setup_entry(
|
|||||||
def _async_device_new(
|
def _async_device_new(
|
||||||
unique_id: str,
|
unique_id: str,
|
||||||
identifier: str,
|
identifier: str,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Signal a new device."""
|
"""Signal a new device."""
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
@ -35,7 +35,7 @@ async def async_setup_entry(
|
|||||||
coordinator,
|
coordinator,
|
||||||
identifier,
|
identifier,
|
||||||
unique_id,
|
unique_id,
|
||||||
parsed,
|
ibeacon_advertisement,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -53,10 +53,12 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity):
|
|||||||
coordinator: IBeaconCoordinator,
|
coordinator: IBeaconCoordinator,
|
||||||
identifier: str,
|
identifier: str,
|
||||||
device_unique_id: str,
|
device_unique_id: str,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an iBeacon tracker entity."""
|
"""Initialize an iBeacon tracker entity."""
|
||||||
super().__init__(coordinator, identifier, device_unique_id, parsed)
|
super().__init__(
|
||||||
|
coordinator, identifier, device_unique_id, ibeacon_advertisement
|
||||||
|
)
|
||||||
self._attr_unique_id = device_unique_id
|
self._attr_unique_id = device_unique_id
|
||||||
self._active = True
|
self._active = True
|
||||||
|
|
||||||
@ -78,11 +80,11 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity):
|
|||||||
@callback
|
@callback
|
||||||
def _async_seen(
|
def _async_seen(
|
||||||
self,
|
self,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update state."""
|
"""Update state."""
|
||||||
self._active = True
|
self._active = True
|
||||||
self._parsed = parsed
|
self._ibeacon_advertisement = ibeacon_advertisement
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -24,12 +24,12 @@ class IBeaconEntity(Entity):
|
|||||||
coordinator: IBeaconCoordinator,
|
coordinator: IBeaconCoordinator,
|
||||||
identifier: str,
|
identifier: str,
|
||||||
device_unique_id: str,
|
device_unique_id: str,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an iBeacon sensor entity."""
|
"""Initialize an iBeacon sensor entity."""
|
||||||
self._device_unique_id = device_unique_id
|
self._device_unique_id = device_unique_id
|
||||||
self._coordinator = coordinator
|
self._coordinator = coordinator
|
||||||
self._parsed = parsed
|
self._ibeacon_advertisement = ibeacon_advertisement
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
name=identifier,
|
name=identifier,
|
||||||
identifiers={(DOMAIN, device_unique_id)},
|
identifiers={(DOMAIN, device_unique_id)},
|
||||||
@ -40,19 +40,19 @@ class IBeaconEntity(Entity):
|
|||||||
self,
|
self,
|
||||||
) -> dict[str, str | int]:
|
) -> dict[str, str | int]:
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
parsed = self._parsed
|
ibeacon_advertisement = self._ibeacon_advertisement
|
||||||
return {
|
return {
|
||||||
ATTR_UUID: str(parsed.uuid),
|
ATTR_UUID: str(ibeacon_advertisement.uuid),
|
||||||
ATTR_MAJOR: parsed.major,
|
ATTR_MAJOR: ibeacon_advertisement.major,
|
||||||
ATTR_MINOR: parsed.minor,
|
ATTR_MINOR: ibeacon_advertisement.minor,
|
||||||
ATTR_SOURCE: parsed.source,
|
ATTR_SOURCE: ibeacon_advertisement.source,
|
||||||
}
|
}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@callback
|
@callback
|
||||||
def _async_seen(
|
def _async_seen(
|
||||||
self,
|
self,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update state."""
|
"""Update state."""
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
|
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
|
||||||
"dependencies": ["bluetooth"],
|
"dependencies": ["bluetooth"],
|
||||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }],
|
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }],
|
||||||
"requirements": ["ibeacon_ble==0.6.4"],
|
"requirements": ["ibeacon_ble==0.7.0"],
|
||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["bleak"],
|
"loggers": ["bleak"],
|
||||||
|
@ -42,7 +42,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
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,
|
||||||
value_fn=lambda parsed: parsed.rssi,
|
value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.rssi,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
IBeaconSensorEntityDescription(
|
IBeaconSensorEntityDescription(
|
||||||
@ -51,7 +51,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
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,
|
||||||
value_fn=lambda parsed: parsed.power,
|
value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.power,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
IBeaconSensorEntityDescription(
|
IBeaconSensorEntityDescription(
|
||||||
@ -59,7 +59,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
name="Estimated Distance",
|
name="Estimated Distance",
|
||||||
icon="mdi:signal-distance-variant",
|
icon="mdi:signal-distance-variant",
|
||||||
native_unit_of_measurement=LENGTH_METERS,
|
native_unit_of_measurement=LENGTH_METERS,
|
||||||
value_fn=lambda parsed: parsed.distance,
|
value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.distance,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -75,7 +75,7 @@ async def async_setup_entry(
|
|||||||
def _async_device_new(
|
def _async_device_new(
|
||||||
unique_id: str,
|
unique_id: str,
|
||||||
identifier: str,
|
identifier: str,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Signal a new device."""
|
"""Signal a new device."""
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
@ -84,7 +84,7 @@ async def async_setup_entry(
|
|||||||
description,
|
description,
|
||||||
identifier,
|
identifier,
|
||||||
unique_id,
|
unique_id,
|
||||||
parsed,
|
ibeacon_advertisement,
|
||||||
)
|
)
|
||||||
for description in SENSOR_DESCRIPTIONS
|
for description in SENSOR_DESCRIPTIONS
|
||||||
)
|
)
|
||||||
@ -105,21 +105,23 @@ class IBeaconSensorEntity(IBeaconEntity, SensorEntity):
|
|||||||
description: IBeaconSensorEntityDescription,
|
description: IBeaconSensorEntityDescription,
|
||||||
identifier: str,
|
identifier: str,
|
||||||
device_unique_id: str,
|
device_unique_id: str,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an iBeacon sensor entity."""
|
"""Initialize an iBeacon sensor entity."""
|
||||||
super().__init__(coordinator, identifier, device_unique_id, parsed)
|
super().__init__(
|
||||||
|
coordinator, identifier, device_unique_id, ibeacon_advertisement
|
||||||
|
)
|
||||||
self._attr_unique_id = f"{device_unique_id}_{description.key}"
|
self._attr_unique_id = f"{device_unique_id}_{description.key}"
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_seen(
|
def _async_seen(
|
||||||
self,
|
self,
|
||||||
parsed: iBeaconAdvertisement,
|
ibeacon_advertisement: iBeaconAdvertisement,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update state."""
|
"""Update state."""
|
||||||
self._attr_available = True
|
self._attr_available = True
|
||||||
self._parsed = parsed
|
self._ibeacon_advertisement = ibeacon_advertisement
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -131,4 +133,4 @@ class IBeaconSensorEntity(IBeaconEntity, SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def native_value(self) -> int | None:
|
def native_value(self) -> int | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self._parsed)
|
return self.entity_description.value_fn(self._ibeacon_advertisement)
|
||||||
|
@ -898,7 +898,7 @@ iammeter==0.1.7
|
|||||||
iaqualink==0.4.1
|
iaqualink==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.ibeacon
|
# homeassistant.components.ibeacon
|
||||||
ibeacon_ble==0.6.4
|
ibeacon_ble==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.watson_tts
|
# homeassistant.components.watson_tts
|
||||||
ibm-watson==5.2.2
|
ibm-watson==5.2.2
|
||||||
|
@ -663,7 +663,7 @@ hyperion-py==0.7.5
|
|||||||
iaqualink==0.4.1
|
iaqualink==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.ibeacon
|
# homeassistant.components.ibeacon
|
||||||
ibeacon_ble==0.6.4
|
ibeacon_ble==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.ping
|
# homeassistant.components.ping
|
||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
|
@ -58,3 +58,46 @@ BEACON_RANDOM_ADDRESS_SERVICE_INFO = BluetoothServiceInfo(
|
|||||||
service_uuids=[],
|
service_uuids=[],
|
||||||
source="local",
|
source="local",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FEASY_BEACON_BLE_DEVICE = BLEDevice(
|
||||||
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
name="FSC-BP108",
|
||||||
|
)
|
||||||
|
|
||||||
|
FEASY_BEACON_SERVICE_INFO_1 = BluetoothServiceInfo(
|
||||||
|
name="FSC-BP108",
|
||||||
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
rssi=-63,
|
||||||
|
manufacturer_data={
|
||||||
|
76: b"\x02\x15\xfd\xa5\x06\x93\xa4\xe2O\xb1\xaf\xcf\xc6\xeb\x07dx%'Qe\xc1\xfd"
|
||||||
|
},
|
||||||
|
service_data={
|
||||||
|
"0000feaa-0000-1000-8000-00805f9b34fb": b' \x00\x0c\x86\x80\x00\x00\x00\x93f\x0b\x7f\x93"',
|
||||||
|
"0000fff0-0000-1000-8000-00805f9b34fb": b"'\x02\x17\x92\xdc\r0\x0e \xbad",
|
||||||
|
},
|
||||||
|
service_uuids=[
|
||||||
|
"0000feaa-0000-1000-8000-00805f9b34fb",
|
||||||
|
"0000fef5-0000-1000-8000-00805f9b34fb",
|
||||||
|
],
|
||||||
|
source="local",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FEASY_BEACON_SERVICE_INFO_2 = BluetoothServiceInfo(
|
||||||
|
name="FSC-BP108",
|
||||||
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
rssi=-63,
|
||||||
|
manufacturer_data={
|
||||||
|
76: b"\x02\x15\xd5F\xdf\x97GWG\xef\xbe\t>-\xcb\xdd\x0cw\xed\xd1;\xd2\xb5"
|
||||||
|
},
|
||||||
|
service_data={
|
||||||
|
"0000feaa-0000-1000-8000-00805f9b34fb": b' \x00\x0c\x86\x80\x00\x00\x00\x93f\x0b\x7f\x93"',
|
||||||
|
"0000fff0-0000-1000-8000-00805f9b34fb": b"'\x02\x17\x92\xdc\r0\x0e \xbad",
|
||||||
|
},
|
||||||
|
service_uuids=[
|
||||||
|
"0000feaa-0000-1000-8000-00805f9b34fb",
|
||||||
|
"0000fef5-0000-1000-8000-00805f9b34fb",
|
||||||
|
],
|
||||||
|
source="local",
|
||||||
|
)
|
||||||
|
@ -20,6 +20,9 @@ from . import (
|
|||||||
BLUECHARM_BEACON_SERVICE_INFO,
|
BLUECHARM_BEACON_SERVICE_INFO,
|
||||||
BLUECHARM_BEACON_SERVICE_INFO_2,
|
BLUECHARM_BEACON_SERVICE_INFO_2,
|
||||||
BLUECHARM_BLE_DEVICE,
|
BLUECHARM_BLE_DEVICE,
|
||||||
|
FEASY_BEACON_BLE_DEVICE,
|
||||||
|
FEASY_BEACON_SERVICE_INFO_1,
|
||||||
|
FEASY_BEACON_SERVICE_INFO_2,
|
||||||
NO_NAME_BEACON_SERVICE_INFO,
|
NO_NAME_BEACON_SERVICE_INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -182,3 +185,62 @@ async def test_can_unload_and_reload(hass):
|
|||||||
assert (
|
assert (
|
||||||
hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state == "2"
|
hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state == "2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_multiple_uuids_same_beacon(hass):
|
||||||
|
"""Test a beacon that broadcasts multiple uuids."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch_all_discovered_devices([FEASY_BEACON_BLE_DEVICE]):
|
||||||
|
inject_bluetooth_service_info(hass, FEASY_BEACON_SERVICE_INFO_1)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance")
|
||||||
|
distance_attributes = distance_sensor.attributes
|
||||||
|
assert distance_sensor.state == "400"
|
||||||
|
assert (
|
||||||
|
distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance"
|
||||||
|
)
|
||||||
|
assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
|
||||||
|
assert distance_attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
|
||||||
|
with patch_all_discovered_devices([FEASY_BEACON_BLE_DEVICE]):
|
||||||
|
inject_bluetooth_service_info(hass, FEASY_BEACON_SERVICE_INFO_2)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance_2")
|
||||||
|
distance_attributes = distance_sensor.attributes
|
||||||
|
assert distance_sensor.state == "0"
|
||||||
|
assert (
|
||||||
|
distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance"
|
||||||
|
)
|
||||||
|
assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
|
||||||
|
assert distance_attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
|
||||||
|
with patch_all_discovered_devices([FEASY_BEACON_BLE_DEVICE]):
|
||||||
|
inject_bluetooth_service_info(hass, FEASY_BEACON_SERVICE_INFO_1)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance")
|
||||||
|
distance_attributes = distance_sensor.attributes
|
||||||
|
assert distance_sensor.state == "400"
|
||||||
|
assert (
|
||||||
|
distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance"
|
||||||
|
)
|
||||||
|
assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
|
||||||
|
assert distance_attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
|
||||||
|
distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance_2")
|
||||||
|
distance_attributes = distance_sensor.attributes
|
||||||
|
assert distance_sensor.state == "0"
|
||||||
|
assert (
|
||||||
|
distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance"
|
||||||
|
)
|
||||||
|
assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
|
||||||
|
assert distance_attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user