From 841bf87b74e60d6900deae026465d74066bd3ca3 Mon Sep 17 00:00:00 2001 From: Ashley Gittins Date: Wed, 13 Nov 2024 03:13:41 +0000 Subject: [PATCH] fix: Scanner config multiple saves. - Fixes Reduce logging - remove save config at info level #364 - Fixes How to remove entities for proxies that no longer exist #214 - Stops Bermuda doing multiple save-outs to config_entry.data for scanner configs. - Fixes a lot of reload-churn during initial startup, Bermuda now only loads once if the scanner setup hasn't changed. - Likely fixes some race-condition-based performance / lockup issues that might have happened very occasionally. - Only save the necessary fields out for scanner info, to avoid ephemeral data causing extra reloads. - This plus the change to scanner identification in previous release resolves being unable to remove entities on devices that relate to non-present scanners (or scanners that are no longer scanners). - Bonus fix, include IRK devices in action `bermuda.dump_devices` when `configured_devices` is set. --- custom_components/bermuda/coordinator.py | 95 ++++++++++++++---------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/custom_components/bermuda/coordinator.py b/custom_components/bermuda/coordinator.py index 26d39e2..44f6a34 100644 --- a/custom_components/bermuda/coordinator.py +++ b/custom_components/bermuda/coordinator.py @@ -162,7 +162,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) @@ -1168,55 +1175,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 = {} @@ -1233,6 +1248,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))