Skip to content

Commit

Permalink
fix: Scanner config multiple saves.
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
agittins committed Nov 13, 2024
1 parent b7f075d commit 841bf87
Showing 1 changed file with 56 additions and 39 deletions.
95 changes: 56 additions & 39 deletions custom_components/bermuda/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 = {}
Expand All @@ -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))
Expand Down

0 comments on commit 841bf87

Please sign in to comment.