diff --git a/custom_components/bermuda/bermuda_device.py b/custom_components/bermuda/bermuda_device.py index 9774fa4..836b9a1 100644 --- a/custom_components/bermuda/bermuda_device.py +++ b/custom_components/bermuda/bermuda_device.py @@ -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): @@ -151,23 +160,16 @@ 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 @@ -175,6 +177,14 @@ def apply_scanner_selection(self, closest_scanner: BermudaDeviceScanner | 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): """ @@ -182,6 +192,7 @@ def calculate_data(self): 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. @@ -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 diff --git a/custom_components/bermuda/bermuda_device_scanner.py b/custom_components/bermuda/bermuda_device_scanner.py index 6913ce0..258b57e 100644 --- a/custom_components/bermuda/bermuda_device_scanner.py +++ b/custom_components/bermuda/bermuda_device_scanner.py @@ -105,6 +105,8 @@ def update_advertisement(self, scandata: BluetoothScannerDevice): claims to have data. """ # In case the scanner has changed it's details since startup: + # FIXME: This should probably be a separate function that the refresh_scanners + # calls if necessary, rather than re-doing it every cycle. self.name = scandata.scanner.name self.area_id = self.scanner_device.area_id self.area_name = self.scanner_device.area_name @@ -240,9 +242,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 @@ -254,12 +259,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): """ diff --git a/custom_components/bermuda/coordinator.py b/custom_components/bermuda/coordinator.py index 5f2c660..26d39e2 100644 --- a/custom_components/bermuda/coordinator.py +++ b/custom_components/bermuda/coordinator.py @@ -926,10 +926,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. @@ -945,8 +947,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.