Skip to content

Commit

Permalink
Merge branch 'main' into optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
agittins authored Nov 13, 2024
2 parents 7b746c9 + e7f9949 commit 4371094
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 73 deletions.
53 changes: 32 additions & 21 deletions custom_components/bermuda/bermuda_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,29 @@ def __init__(self, address, options) -> None:
else:
self.address_type = BDADDR_TYPE_OTHER

def set_ref_power(self, value: float):
def set_ref_power(self, new_ref_power: float):
"""
Set a new reference power for this device and immediately apply
an interim distance calculation.
This gets called by the calibration routines, but also by metadevice
updates, as they need to apply their own ref_power if necessary.
"""
self.ref_power = value
nearest_distance = 9999 # running tally to find closest scanner
nearest_scanner = None
for scanner in self.scanners.values():
rawdist = scanner.set_ref_power(value)
if rawdist < nearest_distance:
nearest_distance = rawdist
nearest_scanner = scanner
if nearest_scanner is not None:
if new_ref_power != self.ref_power:
# it's actually changed, proceed...
self.ref_power = new_ref_power
nearest_distance = 9999 # running tally to find closest scanner
nearest_scanner = None
for scanner in self.scanners.values():
rawdist = scanner.set_ref_power(new_ref_power)
if rawdist is not None and rawdist < nearest_distance:
nearest_distance = rawdist
nearest_scanner = scanner
# Even though the actual scanner should not have changed (it should
# remain none or a given scanner, since the relative distances won't have
# changed due to ref_power), we still call apply so that the new area_distance
# gets applied.
# if nearest_scanner is not None:
self.apply_scanner_selection(nearest_scanner)

def apply_scanner_selection(self, closest_scanner: BermudaDeviceScanner | None):
Expand All @@ -151,37 +160,39 @@ def apply_scanner_selection(self, closest_scanner: BermudaDeviceScanner | None):
Used to apply a "winning" scanner's data to the device for setting closest Area.
"""
# FIXME: This might need to check if it's a metadevice source or dest, and
# ensure things are applied correctly. Might be a non-issue.
old_area = self.area_name
if closest_scanner is not None:
# We found a winner
old_area = self.area_name
self.area_id = closest_scanner.area_id
self.area_name = closest_scanner.area_name
self.area_distance = closest_scanner.rssi_distance
self.area_rssi = closest_scanner.rssi
self.area_scanner = closest_scanner.name
if (old_area != self.area_name) and self.create_sensor:
# We check against area_name so we can know if the
# device's area changed names.
_LOGGER.debug(
"Device %s was in '%s', now in '%s'",
self.name,
old_area,
self.area_name,
)
else:
# Not close to any scanners!
self.area_id = None
self.area_name = None
self.area_distance = None
self.area_rssi = None
self.area_scanner = None
if (old_area != self.area_name) and self.create_sensor:
# Our area has changed!
_LOGGER.debug(
"Device %s was in '%s', now '%s'",
self.name,
old_area,
self.area_name,
)

def calculate_data(self):
"""
Call after doing update_scanner() calls so that distances
etc can be freshly smoothed and filtered.
"""
# Run calculate_data on each child scanner of this device:
for scanner in self.scanners.values():
if isinstance(scanner, BermudaDeviceScanner):
# in issue #355 someone had an empty dict instead of a scanner object.
Expand All @@ -193,7 +204,7 @@ def calculate_data(self):
"scanner_not_instance", "Scanner device is not a BermudaDevice instance, skipping."
)

# Update whether the device has been seen recently, for device_tracker:
# Update whether this device has been seen recently, for device_tracker:
if (
self.last_seen is not None
and MONOTONIC_TIME() - self.options.get(CONF_DEVTRACK_TIMEOUT, DEFAULT_DEVTRACK_TIMEOUT) < self.last_seen
Expand Down
37 changes: 27 additions & 10 deletions custom_components/bermuda/bermuda_device_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,12 @@ def _update_raw_distance(self, reading_is_new=True) -> float:
# Add a new historical reading
self.hist_distance.insert(0, distance)
# don't insert into hist_distance_by_interval, that's done by the caller.
else:
# We are over-riding readings between cycles. Force the
# new value in-place.
elif self.rssi_distance is not None:
# We are over-riding readings between cycles.
# We will force the new measurement, but only if we were
# already showing a "current" distance, as we don't want
# to "freshen" a measurement that was already out of date,
# hence the elif not none above.
self.rssi_distance = distance
if len(self.hist_distance) > 0:
self.hist_distance[0] = distance
Expand All @@ -265,12 +268,25 @@ def _update_raw_distance(self, reading_is_new=True) -> float:
# modify in-place.
return distance

def set_ref_power(self, value: float):
"""Set a new reference power from the parent device and immediately update distance."""
def set_ref_power(self, value: float) -> float | None:
"""
Set a new reference power and return the resulting distance.
Typically called from the parent device when either the user changes the calibration
of ref_power for a device, or when a metadevice takes on a new source device, and
propagates its own ref_power to our parent.
Note that it is unlikely to return None as its only returning the raw, not filtered
distance = the exception being uninitialised entries.
"""
# When the user updates the ref_power we want to reflect that change immediately,
# and not subject it to the normal smoothing algo.
self.ref_power = value
return self._update_raw_distance(False)
# But make sure it's actually different, in case it's just a metadevice propagating
# its own ref_power without need.
if value != self.ref_power:
self.ref_power = value
return self._update_raw_distance(False)
return self.rssi_distance_raw

def calculate_data(self):
"""
Expand Down Expand Up @@ -388,9 +404,10 @@ def calculate_data(self):
velocity,
)
# Discard the bogus reading by duplicating the last.
self.hist_distance_by_interval.insert(0, self.hist_distance_by_interval[0])
elif len(self.hist_distance_by_interval) == 0:
self.hist_distance_by_interval = [self.rssi_distance_raw]
if len(self.hist_distance_by_interval) == 0:
self.hist_distance_by_interval = [self.rssi_distance_raw]
else:
self.hist_distance_by_interval.insert(0, self.hist_distance_by_interval[0])
else:
self.hist_distance_by_interval.insert(0, self.hist_distance_by_interval[0])

Expand Down
119 changes: 77 additions & 42 deletions custom_components/bermuda/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,14 @@ def __init__(
self.metadevices: dict[str, BermudaDevice] = {}

self._ad_listener_cancel: Cancellable | None = None
self.last_config_entry_update: float = 0

# Tracks the last stamp that we *actually* saved our config entry. Mostly for debugging,
# we use a request stamp for tracking our add_job request.
self.last_config_entry_update: float = 0 # Stamp of last *save-out* of config.data

# We want to delay the first save-out, since it takes a few seconds for things
# to stabilise. So set the stamp into the future.
self.last_config_entry_update_request = MONOTONIC_TIME() + SAVEOUT_COOLDOWN # Stamp for save-out requests

self.hass.bus.async_listen(EVENT_STATE_CHANGED, self.handle_state_changes)

Expand Down Expand Up @@ -941,10 +948,12 @@ def update_metadevices(self):
other initialisation.
"""
# First seed the metadevice skeletons and set their latest beacon_source entries
# Private BLE Devices:
# Private BLE Devices. It will only do anything if the self._do_private_device_init
# flag is set.
self.discover_private_ble_metadevices()

# iBeacon devices should already have their metadevices created.
# FIXME: irk and ibeacons will fight over their relative ref_power too.

for metadev in self.metadevices.values():
# We Expect the first beacon source to be the current one.
Expand All @@ -960,8 +969,24 @@ def update_metadevices(self):
# Map the source device's scanner list into ours
metadev.scanners = source_device.scanners

# Set the source device's ref_power from our own
source_device.set_ref_power(metadev.ref_power)
# Set the source device's ref_power from our own. This will cause
# the source device and all its scanner entries to update their
# distance measurements. This won't affect Area wins though, because
# they are "relative", not absolute.

# FIXME: This has two potential bugs:
# - if multiple metadevices share a source, they will
# "fight" over their preferred ref_power, if different.
# - The non-meta device (if tracked) will receive distances
# based on the meta device's ref_power.
# - The non-meta device if tracked will have its own ref_power ignored.
#
# None of these are terribly awful, but worth fixing.

# Note we are setting the ref_power on the source_device, not the
# individual scanner entries (it will propagate to them though)
if source_device.ref_power != metadev.ref_power:
source_device.set_ref_power(metadev.ref_power)

# anything that isn't already set to something interesting, overwrite
# it with the new device's data.
Expand Down Expand Up @@ -1165,55 +1190,63 @@ def _refresh_scanners(self, scanners: list[BluetoothScannerDevice] | None = None
confdata_scanners: dict[str, dict] = {}
for device in self.devices.values():
if device.is_scanner:
confdata_scanners[device.address] = device.to_dict()
self.scanner_list.append(device.address)
# Only add the necessary fields to confdata
confdata_scanners[device.address] = {
key: getattr(device, key)
for key in [
"name",
"local_name",
"prefname",
"address",
"ref_power",
"unique_id",
"address_type",
"area_id",
"area_name",
"is_scanner",
"entry_id",
]
}

if self.config_entry.data.get(CONFDATA_SCANNERS, {}) == confdata_scanners:
# **** BAIL OUT, CONFIG HAS NOT CHANGED ****
# _LOGGER.debug("Scanner configs are identical, not doing update.")
# Return true since we're happy that the config entry
# exists and has the current scanner data that we want,
# so there's nothing to do.
# See #351, #341
self._do_full_scanner_init = False
return True

# _LOGGER.debug(
# "Replacing config data scanners was %s now %s",
# self.config_entry.data.get(CONFDATA_SCANNERS, {}),
# confdata_scanners,
# )

@callback
def async_call_update_entry() -> None:
"""
Call in the event loop to update the scanner entries in our config.
We do this via add_job to ensure it runs in the event loop.
"""
if self.last_config_entry_update > MONOTONIC_TIME() - SAVEOUT_COOLDOWN:
# We are probably not the only instance of ourselves in this queue.
# let's back off for a bit.
return
self.last_config_entry_update = MONOTONIC_TIME()
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONFDATA_SCANNERS: confdata_scanners,
},
)
# Clear the flag for init
self._do_full_scanner_init = False
# We will arrive here every second for as long as the saved config is
# different from our running config. But we don't want to save immediately,
# since there is a lot of bouncing that happens during setup.

# After calling the update there are a lot of cycles while loading etc.
# Cool off for a little before calling again...
if self.last_config_entry_update < MONOTONIC_TIME() - SAVEOUT_COOLDOWN:
self.last_config_entry_update = MONOTONIC_TIME()
_LOGGER.info("Saving out scanner configs")
self.hass.add_job(async_call_update_entry)
# Make sure we haven't requested recently...
if self.last_config_entry_update_request < MONOTONIC_TIME() - SAVEOUT_COOLDOWN:
# OK, we're good to go.
self.last_config_entry_update_request = MONOTONIC_TIME()
_LOGGER.debug("Requesting save-out of scanner configs")
self.hass.add_job(self.async_call_update_entry, confdata_scanners)

return True

@callback
def async_call_update_entry(self, confdata_scanners) -> None:
"""
Call in the event loop to update the scanner entries in our config.
We do this via add_job to ensure it runs in the event loop.
"""
# Clear the flag for init and update the stamp
self._do_full_scanner_init = False
self.last_config_entry_update = MONOTONIC_TIME()
# Apply new config (will cause reload if there are changes)
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONFDATA_SCANNERS: confdata_scanners,
},
)

async def service_dump_devices(self, call: ServiceCall) -> ServiceResponse: # pylint: disable=unused-argument;
"""Return a dump of beacon advertisements by receiver."""
out = {}
Expand All @@ -1230,6 +1263,8 @@ async def service_dump_devices(self, call: ServiceCall) -> ServiceResponse: # p
# configured and scanners
addresses += self.scanner_list
addresses += self.options.get(CONF_DEVICES, [])
# known IRK/Private BLE Devices
addresses += self.pb_state_sources

# lowercase all the addresses for matching
addresses = list(map(str.lower, addresses))
Expand Down
Loading

0 comments on commit 4371094

Please sign in to comment.