Update ibeacon-ble to 1.0.1 (#80785)

This commit is contained in:
J. Nick Koston 2022-10-26 03:21:30 -05:00 committed by GitHub
parent a90ef3a575
commit e15f2e050e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 31 deletions

View File

@ -13,7 +13,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bluetooth LE Tracker from a config entry."""
coordinator = hass.data[DOMAIN] = IBeaconCoordinator(hass, entry, async_get(hass))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
coordinator.async_start()
await coordinator.async_start()
return True

View File

@ -2,6 +2,9 @@
from datetime import timedelta
from homeassistant.components.bluetooth import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
)
from homeassistant.const import Platform
DOMAIN = "ibeacon"
@ -31,5 +34,15 @@ MAX_IDS = 10
# we will add it to the ignore list since its garbage data.
MAX_IDS_PER_UUID = 50
# Number of times a beacon must be seen before it is added to the system
# This is to prevent devices that are just passing by from being added
# to the system.
MIN_SEEN_TRANSIENT_NEW = (
round(
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS / UPDATE_INTERVAL.total_seconds()
)
+ 1
)
CONF_IGNORE_ADDRESSES = "ignore_addresses"
CONF_IGNORE_UUIDS = "ignore_uuids"

View File

@ -9,8 +9,7 @@ from ibeacon_ble import (
IBEACON_FIRST_BYTE,
IBEACON_SECOND_BYTE,
iBeaconAdvertisement,
is_ibeacon_service_info,
parse,
iBeaconParser,
)
from homeassistant.components import bluetooth
@ -27,6 +26,7 @@ from .const import (
DOMAIN,
MAX_IDS,
MAX_IDS_PER_UUID,
MIN_SEEN_TRANSIENT_NEW,
SIGNAL_IBEACON_DEVICE_NEW,
SIGNAL_IBEACON_DEVICE_SEEN,
SIGNAL_IBEACON_DEVICE_UNAVAILABLE,
@ -111,6 +111,7 @@ class IBeaconCoordinator:
self.hass = hass
self._entry = entry
self._dev_reg = registry
self._ibeacon_parser = iBeaconParser()
# iBeacon devices that do not follow the spec
# and broadcast custom data in the major and minor fields
@ -125,6 +126,7 @@ class IBeaconCoordinator:
self._last_ibeacon_advertisement_by_unique_id: dict[
str, iBeaconAdvertisement
] = {}
self._transient_seen_count: dict[str, int] = {}
self._group_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]] = {}
@ -161,6 +163,7 @@ class IBeaconCoordinator:
def _async_cancel_unavailable_tracker(self, address: str) -> None:
"""Cancel unavailable tracking for an address."""
self._unavailable_trackers.pop(address)()
self._transient_seen_count.pop(address, None)
@callback
def _async_ignore_uuid(self, uuid: str) -> None:
@ -236,7 +239,7 @@ class IBeaconCoordinator:
"""Update from a bluetooth callback."""
if service_info.address in self._ignore_addresses:
return
if not (ibeacon_advertisement := parse(service_info)):
if not (ibeacon_advertisement := self._ibeacon_parser.parse(service_info)):
return
uuid_str = str(ibeacon_advertisement.uuid)
@ -297,12 +300,21 @@ class IBeaconCoordinator:
or service_info.device.name.replace("-", ":") == service_info.device.address
):
return
previously_tracked = address in self._unique_ids_by_address
self._last_ibeacon_advertisement_by_unique_id[unique_id] = ibeacon_advertisement
self._async_track_ibeacon_with_unique_address(address, group_id, unique_id)
if address not in self._unavailable_trackers:
self._unavailable_trackers[address] = bluetooth.async_track_unavailable(
self.hass, self._async_handle_unavailable, address
)
if not previously_tracked and new and ibeacon_advertisement.transient:
# Do not create a new tracker right away for transient devices
# If they keep advertising, we will create entities for them
# once _async_update_rssi_and_transients has seen them enough times
self._transient_seen_count[address] = 1
return
# Some manufacturers violate the spec and flood us with random
# data (sometimes its temperature data).
#
@ -349,23 +361,42 @@ class IBeaconCoordinator:
async_dispatcher_send(self.hass, signal_unavailable(group_id))
@callback
def _async_update_rssi(self) -> None:
def _async_update_rssi_and_transients(self) -> None:
"""Check to see if the rssi has changed and update any devices.
We don't callback on RSSI changes so we need to check them
here and send them over the dispatcher periodically to
ensure the distance calculation is update.
If the transient flag is set we also need to check to see
if the device is still transmitting and increment the counter
"""
for (
unique_id,
ibeacon_advertisement,
) in self._last_ibeacon_advertisement_by_unique_id.items():
address = unique_id.split("_")[-1]
if (
service_info := bluetooth.async_last_service_info(
self.hass, address, connectable=False
)
) and service_info.rssi != ibeacon_advertisement.rssi:
service_info = bluetooth.async_last_service_info(
self.hass, address, connectable=False
)
if not service_info:
continue
if address in self._transient_seen_count:
self._transient_seen_count[address] += 1
if self._transient_seen_count[address] == MIN_SEEN_TRANSIENT_NEW:
self._transient_seen_count.pop(address)
_async_dispatch_update(
self.hass,
unique_id,
service_info,
ibeacon_advertisement,
True,
True,
)
continue
if service_info.rssi != ibeacon_advertisement.rssi:
ibeacon_advertisement.update_rssi(service_info.rssi)
async_dispatcher_send(
self.hass,
@ -377,7 +408,7 @@ class IBeaconCoordinator:
def _async_update(self, _now: datetime) -> None:
"""Update the Coordinator."""
self._async_check_unavailable_groups_with_random_macs()
self._async_update_rssi()
self._async_update_rssi_and_transients()
@callback
def _async_restore_from_registry(self) -> None:
@ -403,9 +434,9 @@ class IBeaconCoordinator:
group_id = f"{uuid}_{major}_{minor}"
self._group_ids_random_macs.add(group_id)
@callback
def async_start(self) -> None:
async def async_start(self) -> None:
"""Start the Coordinator."""
await self._ibeacon_parser.async_setup()
self._async_restore_from_registry()
entry = self._entry
entry.async_on_unload(
@ -421,14 +452,6 @@ class IBeaconCoordinator:
)
)
entry.async_on_unload(self._async_stop)
# Replay any that are already there.
for service_info in bluetooth.async_discovered_service_info(
self.hass, connectable=False
):
if is_ibeacon_service_info(service_info):
self._async_update_ibeacon(
service_info, bluetooth.BluetoothChange.ADVERTISEMENT
)
entry.async_on_unload(
async_track_time_interval(self.hass, self._async_update, UPDATE_INTERVAL)
)

View File

@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
"dependencies": ["bluetooth"],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }],
"requirements": ["ibeacon_ble==0.7.4"],
"requirements": ["ibeacon_ble==1.0.1"],
"codeowners": ["@bdraco"],
"iot_class": "local_push",
"loggers": ["bleak"],

View File

@ -27,7 +27,7 @@ from .entity import IBeaconEntity
class IBeaconRequiredKeysMixin:
"""Mixin for required keys."""
value_fn: Callable[[iBeaconAdvertisement], int | None]
value_fn: Callable[[iBeaconAdvertisement], str | int | None]
@dataclass
@ -63,6 +63,12 @@ SENSOR_DESCRIPTIONS = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DISTANCE,
),
IBeaconSensorEntityDescription(
key="vendor",
name="Vendor",
entity_registry_enabled_default=False,
value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.vendor,
),
)
@ -132,6 +138,6 @@ class IBeaconSensorEntity(IBeaconEntity, SensorEntity):
self.async_write_ha_state()
@property
def native_value(self) -> int | None:
def native_value(self) -> str | int | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._ibeacon_advertisement)

View File

@ -901,7 +901,7 @@ iammeter==0.1.7
iaqualink==0.5.0
# homeassistant.components.ibeacon
ibeacon_ble==0.7.4
ibeacon_ble==1.0.1
# homeassistant.components.watson_tts
ibm-watson==5.2.2

View File

@ -672,7 +672,7 @@ hyperion-py==0.7.5
iaqualink==0.5.0
# homeassistant.components.ibeacon
ibeacon_ble==0.7.4
ibeacon_ble==1.0.1
# homeassistant.components.ping
icmplib==3.0

View File

@ -58,7 +58,21 @@ BEACON_RANDOM_ADDRESS_SERVICE_INFO = BluetoothServiceInfo(
service_uuids=[],
source="local",
)
TESLA_TRANSIENT = BluetoothServiceInfo(
address="CC:CC:CC:CC:CC:CC",
rssi=-60,
name="S6da7c9389bd5452cC",
manufacturer_data={
76: b"\x02\x15t'\x8b\xda\xb6DE \x8f\x0cr\x0e\xaf\x05\x995\x00\x00[$\xc5"
},
service_data={},
service_uuids=[],
source="hci0",
)
TESLA_TRANSIENT_BLE_DEVICE = BLEDevice(
address="CC:CC:CC:CC:CC:CC",
name="S6da7c9389bd5452cC",
)
FEASY_BEACON_BLE_DEVICE = BLEDevice(
address="AA:BB:CC:DD:EE:FF",

View File

@ -2,16 +2,27 @@
from dataclasses import replace
from datetime import timedelta
import pytest
from homeassistant.components.ibeacon.const import DOMAIN
from homeassistant.components.ibeacon.const import DOMAIN, UPDATE_INTERVAL
from homeassistant.const import STATE_HOME
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from homeassistant.util import dt as dt_util
from . import BLUECHARM_BEACON_SERVICE_INFO, BLUECHARM_BEACON_SERVICE_INFO_DBUS
from . import (
BLUECHARM_BEACON_SERVICE_INFO,
BLUECHARM_BEACON_SERVICE_INFO_DBUS,
TESLA_TRANSIENT,
TESLA_TRANSIENT_BLE_DEVICE,
)
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import (
inject_bluetooth_service_info,
patch_all_discovered_devices,
)
@pytest.fixture(autouse=True)
@ -195,3 +206,49 @@ async def test_rotating_major_minor_and_mac_no_name(hass):
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count
async def test_ignore_transient_devices_unless_we_see_them_a_few_times(hass):
"""Test we ignore transient devices unless we see them a few times."""
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()
before_entity_count = len(hass.states.async_entity_ids())
inject_bluetooth_service_info(
hass,
TESLA_TRANSIENT,
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == before_entity_count
with patch_all_discovered_devices([TESLA_TRANSIENT_BLE_DEVICE]):
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2),
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == before_entity_count
for i in range(3, 17):
with patch_all_discovered_devices([TESLA_TRANSIENT_BLE_DEVICE]):
async_fire_time_changed(
hass,
dt_util.utcnow()
+ timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2 * i),
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) > before_entity_count
assert hass.states.get("device_tracker.s6da7c9389bd5452cc_cccc").state == STATE_HOME
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.s6da7c9389bd5452cc_cccc").state == STATE_HOME