From c7fdc394321afffb0e8abb5a39ce7d1942ac0f4d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 09:35:23 +0200 Subject: [PATCH 01/23] Add extra logging for checking the energy-logs in memory --- plugwise_usb/nodes/circle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index ae4e095ac..d0bb382cf 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -725,9 +725,15 @@ async def _energy_log_record_update_state( import_only: bool = False, ) -> bool: """Process new energy log record. Returns true if record is new or changed.""" + _LOGGER.warning( + "EnergyLogs before update: %s", self._energy_counters.get_pulse_logs() + ) self._energy_counters.add_pulse_log( address, slot, timestamp, pulses, import_only=import_only ) + _LOGGER.warning( + "EnergyLogs after update: %s", self._energy_counters.get_pulse_logs() + ) if not self._cache_enabled: return False From 415f83bab2154df34d5768be366dc2471f586707 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 09:47:27 +0200 Subject: [PATCH 02/23] Save energy-logs in memory to cache instead of adding to the cache-file preventing the storage of outdated data in the cache-file --- plugwise_usb/nodes/circle.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index d0bb382cf..26134337e 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -749,12 +749,7 @@ async def _energy_log_record_update_state( str(slot), self._mac_in_str, ) - new_cache = ( - f"{log_cache_record}|{cached_logs}" - if cached_logs - else log_cache_record - ) - self._set_cache(CACHE_ENERGY_COLLECTION, new_cache) + self._energy_log_records_save_to_cache() await self.save_cache(trigger_only=True) return True @@ -769,7 +764,8 @@ async def _energy_log_record_update_state( str(slot), self._mac_in_str, ) - self._set_cache(CACHE_ENERGY_COLLECTION, log_cache_record) + self._energy_log_records_save_to_cache() + await self.save_cache(trigger_only=True) return True @raise_not_loaded From 3b925b0f6810b33dfbd8f03b87ed1717307a2be9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 09:48:54 +0200 Subject: [PATCH 03/23] Bump to v0.44.14a0 for testing --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0999c77ed..9784ef864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.13" +version = "0.44.14a0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From d8b41c7400ad6e935527692c3968f1270c29ba4e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 10:13:09 +0200 Subject: [PATCH 04/23] Remove loggers, add missing awaits, avoid excessive cache-writing --- plugwise_usb/nodes/circle.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 26134337e..ed0e7cc98 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -529,9 +529,6 @@ async def _get_initial_energy_logs(self) -> None: log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 - if self._cache_enabled: - await self._energy_log_records_save_to_cache() - async def get_missing_energy_logs(self) -> None: """Task to retrieve missing energy logs.""" self._energy_counters.update() @@ -565,9 +562,6 @@ async def get_missing_energy_logs(self) -> None: await gather(*to_cancel, return_exceptions=True) break - if self._cache_enabled: - await self._energy_log_records_save_to_cache() - async def energy_log_update( self, address: int | None, save_cache: bool = True ) -> bool: @@ -713,8 +707,6 @@ async def _energy_log_records_save_to_cache(self) -> None: cached_logs = "|".join(records) _LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str) self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs) - # Persist new cache entries to disk immediately - await self.save_cache(trigger_only=True) async def _energy_log_record_update_state( self, @@ -725,15 +717,9 @@ async def _energy_log_record_update_state( import_only: bool = False, ) -> bool: """Process new energy log record. Returns true if record is new or changed.""" - _LOGGER.warning( - "EnergyLogs before update: %s", self._energy_counters.get_pulse_logs() - ) self._energy_counters.add_pulse_log( address, slot, timestamp, pulses, import_only=import_only ) - _LOGGER.warning( - "EnergyLogs after update: %s", self._energy_counters.get_pulse_logs() - ) if not self._cache_enabled: return False @@ -749,8 +735,7 @@ async def _energy_log_record_update_state( str(slot), self._mac_in_str, ) - self._energy_log_records_save_to_cache() - await self.save_cache(trigger_only=True) + await self._energy_log_records_save_to_cache() return True _LOGGER.debug( @@ -764,8 +749,7 @@ async def _energy_log_record_update_state( str(slot), self._mac_in_str, ) - self._energy_log_records_save_to_cache() - await self.save_cache(trigger_only=True) + await self._energy_log_records_save_to_cache() return True @raise_not_loaded From 791e64e2bdfe8f1b87d0809e916756bac5c97c67 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 10:29:58 +0200 Subject: [PATCH 05/23] Save cache at suitable moments. --- plugwise_usb/nodes/circle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index ed0e7cc98..0e57a02ba 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -441,6 +441,8 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return None + await self.save_cache() + if ( missing_addresses := self._energy_counters.log_addresses_missing ) is not None: @@ -529,6 +531,8 @@ async def _get_initial_energy_logs(self) -> None: log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 + await self.save_cache() + async def get_missing_energy_logs(self) -> None: """Task to retrieve missing energy logs.""" self._energy_counters.update() @@ -562,6 +566,8 @@ async def get_missing_energy_logs(self) -> None: await gather(*to_cancel, return_exceptions=True) break + await self.save_cache() + async def energy_log_update( self, address: int | None, save_cache: bool = True ) -> bool: From d26521aa3b84d2ac1edcceb39b4e7804fa816f8e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 10:48:01 +0200 Subject: [PATCH 06/23] Bump to a1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9784ef864..fec003070 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.14a0" +version = "0.44.14a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 2d7317a617ba902151117a9c8408c0e2538a1389 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 11:18:32 +0200 Subject: [PATCH 07/23] Guard save_cache() where needed --- plugwise_usb/nodes/circle.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 0e57a02ba..658f6f43f 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -441,7 +441,8 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return None - await self.save_cache() + if self._cache_enabled: + await self.save_cache() if ( missing_addresses := self._energy_counters.log_addresses_missing @@ -531,7 +532,8 @@ async def _get_initial_energy_logs(self) -> None: log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 - await self.save_cache() + if self._cache_enabled: + await self.save_cache() async def get_missing_energy_logs(self) -> None: """Task to retrieve missing energy logs.""" @@ -566,7 +568,8 @@ async def get_missing_energy_logs(self) -> None: await gather(*to_cancel, return_exceptions=True) break - await self.save_cache() + if self._cache_enabled: + await self.save_cache() async def energy_log_update( self, address: int | None, save_cache: bool = True From 2d25c76ee07f952ba69d932192ee0848505797da Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 11:29:03 +0200 Subject: [PATCH 08/23] Minimize cache-writes --- plugwise_usb/nodes/circle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 658f6f43f..9b5cfcb93 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -418,7 +418,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 # Try collecting energy-stats for _current_log_address result = await self.energy_log_update( - self._current_log_address, save_cache=True + self._current_log_address, save_cache=False ) if not result: _LOGGER.debug( @@ -432,7 +432,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 # Retry with previous log address as Circle node pointer to self._current_log_address # could be rolled over while the last log is at previous address/slot prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4) - result = await self.energy_log_update(prev_log_address, save_cache=True) + result = await self.energy_log_update(prev_log_address, save_cache=False) if not result: _LOGGER.debug( "async_energy_update | %s | Log rollover | energy_log_update from address %s failed", From ecd88898ae420cf6ca22116189098992be95e2ee Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 11:34:40 +0200 Subject: [PATCH 09/23] Fix setting cache_updated state --- plugwise_usb/nodes/circle.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 9b5cfcb93..a91d7636a 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -432,7 +432,9 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 # Retry with previous log address as Circle node pointer to self._current_log_address # could be rolled over while the last log is at previous address/slot prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4) - result = await self.energy_log_update(prev_log_address, save_cache=False) + result = await self.energy_log_update( + prev_log_address, save_cache=False + ) if not result: _LOGGER.debug( "async_energy_update | %s | Log rollover | energy_log_update from address %s failed", @@ -612,13 +614,15 @@ async def energy_log_update( self._energy_counters.add_empty_log(response.log_address, _slot) continue - cache_updated = await self._energy_log_record_update_state( + updated = await self._energy_log_record_update_state( response.log_address, _slot, log_timestamp.replace(tzinfo=UTC), log_pulses, import_only=True, ) + if updated: + cache_updated = True self._energy_counters.update() if cache_updated and save_cache: From c1e08d6970654827a12ba5b54d285ada724f2a38 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 11:44:20 +0200 Subject: [PATCH 10/23] Optimize use of _energy_log_records_save_to_cache() as suggested --- plugwise_usb/nodes/circle.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index a91d7636a..133e07e89 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -625,11 +625,13 @@ async def energy_log_update( cache_updated = True self._energy_counters.update() - if cache_updated and save_cache: - _LOGGER.debug( - "Saving energy record update to cache for %s", self._mac_in_str - ) - await self.save_cache() + if cache_updated: + await self._energy_log_records_save_to_cache() + if save_cache: + _LOGGER.debug( + "Saving energy record update to cache for %s", self._mac_in_str + ) + await self.save_cache() return True @@ -748,7 +750,6 @@ async def _energy_log_record_update_state( str(slot), self._mac_in_str, ) - await self._energy_log_records_save_to_cache() return True _LOGGER.debug( @@ -762,7 +763,6 @@ async def _energy_log_record_update_state( str(slot), self._mac_in_str, ) - await self._energy_log_records_save_to_cache() return True @raise_not_loaded From a5a17ee0a4fe0e5b17e0710d39d81d7e4f662168 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 11:49:30 +0200 Subject: [PATCH 11/23] Update docstring, fix typo as suggested --- plugwise_usb/nodes/circle.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 133e07e89..5cad61d48 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -576,7 +576,10 @@ async def get_missing_energy_logs(self) -> None: async def energy_log_update( self, address: int | None, save_cache: bool = True ) -> bool: - """Request energy logs and return True only when at least one recent, non-empty record was stored; otherwise return False.""" + """Request energy logs from node and store them. + + Return True if processing succeeded (records stored in memory), regardless of whether new entries were added. + Return False on transport or address errors.""" if address is None: return False @@ -1186,7 +1189,7 @@ async def _relay_init_update_state(self, state: bool) -> None: NodeFeature.RELAY_INIT, self._relay_config ) _LOGGER.debug( - "Saving relay_init state update to cachefor %s", self._mac_in_str + "Saving relay_init state update to cache for %s", self._mac_in_str ) await self.save_cache() From 31b8f3950657229175e971ec5c6aa7d5e7a3a239 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sat, 30 Aug 2025 09:50:09 +0000 Subject: [PATCH 12/23] fixup: energy_log_cache Python code reformatted using Ruff --- plugwise_usb/nodes/circle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 5cad61d48..d131d139e 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -577,9 +577,10 @@ async def energy_log_update( self, address: int | None, save_cache: bool = True ) -> bool: """Request energy logs from node and store them. - + Return True if processing succeeded (records stored in memory), regardless of whether new entries were added. - Return False on transport or address errors.""" + Return False on transport or address errors. + """ if address is None: return False From 47ea2cc094ad9ffef831f70f81323b5d57a7ce31 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 11:57:49 +0200 Subject: [PATCH 13/23] Bump to a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fec003070..6284d8358 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.14a1" +version = "0.44.14a2" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From c1b221ba2d3bc274d14ad6fa774161fb3bf73c47 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 12:32:38 +0200 Subject: [PATCH 14/23] Implement suggested improvements --- plugwise_usb/nodes/circle.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index d131d139e..f396b996a 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -618,22 +618,21 @@ async def energy_log_update( self._energy_counters.add_empty_log(response.log_address, _slot) continue - updated = await self._energy_log_record_update_state( + slot_updated = await self._energy_log_record_update_state( response.log_address, _slot, log_timestamp.replace(tzinfo=UTC), log_pulses, import_only=True, ) - if updated: - cache_updated = True + cache_updated |= slot_updated self._energy_counters.update() if cache_updated: await self._energy_log_records_save_to_cache() if save_cache: _LOGGER.debug( - "Saving energy record update to cache for %s", self._mac_in_str + "Saving and storing energy cache for %s", self._mac_in_str ) await self.save_cache() @@ -708,7 +707,7 @@ async def _energy_log_records_load_from_cache(self) -> bool: return True async def _energy_log_records_save_to_cache(self) -> None: - """Save currently collected energy logs to cached file.""" + """Update the in-memory energy log cache string (no file I/O).""" if not self._cache_enabled: return @@ -724,7 +723,7 @@ async def _energy_log_records_save_to_cache(self) -> None: f"{address}:{slot}:{ts.strftime('%Y-%m-%d-%H-%M-%S')}:{log.pulses}" ) cached_logs = "|".join(records) - _LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str) + _LOGGER.debug("Updating in-memory energy log records for %s", self._mac_in_str) self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs) async def _energy_log_record_update_state( From ae4d9395d32ff81709e97b992872f34e40545e2e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 13:01:29 +0200 Subject: [PATCH 15/23] energy_log_update(): improve docstring --- plugwise_usb/nodes/circle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index f396b996a..68f3ea779 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -578,7 +578,7 @@ async def energy_log_update( ) -> bool: """Request energy logs from node and store them. - Return True if processing succeeded (records stored in memory), regardless of whether new entries were added. + Return True if processing succeeded: records stored in memory, also for empty slots. Return False on transport or address errors. """ if address is None: From 70d4992026a87c4281ad6bcb5a07df85f1b362d3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 30 Aug 2025 13:44:50 +0200 Subject: [PATCH 16/23] Rework energy_log_update() by combining previously used and recent code --- plugwise_usb/nodes/circle.py | 130 +++++++++++++---------------------- 1 file changed, 47 insertions(+), 83 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 68f3ea779..f87dd62f1 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -401,9 +401,10 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 self._log_no_energy_stats_update() return None - # Always request last energy log records at initial startup + # Always request the most recent energy log records at initial startup, check if the current + # address is acutally reported by the node even when all slots at that address are empty. if not self._last_energy_log_requested: - self._last_energy_log_requested = await self.energy_log_update( + self._last_energy_log_requested, _ = await self.energy_log_update( self._current_log_address, save_cache=False ) @@ -416,8 +417,8 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return None - # Try collecting energy-stats for _current_log_address - result = await self.energy_log_update( + # Try collecting energy-stats from _current_log_address + result, _ = await self.energy_log_update( self._current_log_address, save_cache=False ) if not result: @@ -428,20 +429,17 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return None - if self._current_log_address is not None: - # Retry with previous log address as Circle node pointer to self._current_log_address - # could be rolled over while the last log is at previous address/slot - prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4) - result = await self.energy_log_update( - prev_log_address, save_cache=False + # Retry with previous log address as Circle node pointer to self._current_log_address + # could be rolled over while the last log is at previous address + prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4) + result, _ = await self.energy_log_update(prev_log_address, save_cache=False) + if not result: + _LOGGER.debug( + "async_energy_update | %s | Log rollover | energy_log_update from address %s failed", + self._mac_in_str, + prev_log_address, ) - if not result: - _LOGGER.debug( - "async_energy_update | %s | Log rollover | energy_log_update from address %s failed", - self._mac_in_str, - prev_log_address, - ) - return None + return None if self._cache_enabled: await self.save_cache() @@ -458,7 +456,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 return self._energy_counters.energy_statistics if len(missing_addresses) == 1: - result = await self.energy_log_update( + result, _ = await self.energy_log_update( missing_addresses[0], save_cache=True ) if result: @@ -521,8 +519,11 @@ async def _get_initial_energy_logs(self) -> None: ) log_address = self._current_log_address while total_addresses > 0: - result = await self.energy_log_update(log_address, save_cache=False) - if not result: + result, slots_empty = await self.energy_log_update( + log_address, save_cache=False + ) + if result and not slots_empty: + # (address with outdated data in slots is stored as with None data!) # Stop initial log collection when an address contains no (None) or outdated data # Outdated data can indicate a EnergyLog address rollover: from address 6014 to 0 _LOGGER.debug( @@ -575,14 +576,17 @@ async def get_missing_energy_logs(self) -> None: async def energy_log_update( self, address: int | None, save_cache: bool = True - ) -> bool: + ) -> tuple[bool, bool]: """Request energy logs from node and store them. - Return True if processing succeeded: records stored in memory, also for empty slots. - Return False on transport or address errors. + Return first bool as True if processing succeeded: records stored in memory, also with empty slots. + Return fist bool as False on transport or address errors. + Return second bool as False when all slots are empty otherwise as True """ + result = False + slots_empty = True if address is None: - return False + return result, slots_empty _LOGGER.debug( "Requesting EnergyLogs from node %s address %s", @@ -595,8 +599,9 @@ async def energy_log_update( "Retrieving EnergyLogs data from node %s failed", self._mac_in_str, ) - return False + return result, slots_empty + result = True _LOGGER.debug("EnergyLogs from node %s, address=%s:", self._mac_in_str, address) await self._available_update_state(True, response.timestamp) @@ -604,39 +609,40 @@ async def energy_log_update( # Each response message contains 4 log counters (slots) of the # energy pulses collected during the previous hour of given timestamp cache_updated = False + slot_updated = False for _slot in range(4, 0, -1): log_timestamp, log_pulses = response.log_data[_slot] _LOGGER.debug( "In slot=%s: pulses=%s, timestamp=%s", _slot, log_pulses, log_timestamp ) - if ( - log_timestamp is None - or log_pulses is None - # Don't store an old log record; store an empty record instead - or not self._check_timestamp_is_recent(address, _slot, log_timestamp) - ): + if log_timestamp is None or log_pulses is None: self._energy_counters.add_empty_log(response.log_address, _slot) - continue - - slot_updated = await self._energy_log_record_update_state( - response.log_address, - _slot, - log_timestamp.replace(tzinfo=UTC), - log_pulses, - import_only=True, - ) + elif self._check_timestamp_is_recent( + response.log_address, _slot, log_timestamp.replace(tzinfo=UTC) + ): + self._energy_counters.add_pulse_log( + response.log_address, + _slot, + log_timestamp.replace(tzinfo=UTC), + log_pulses, + import_only=True, + ) + slot_updated = True + cache_updated |= slot_updated self._energy_counters.update() if cache_updated: + slots_empty = False await self._energy_log_records_save_to_cache() if save_cache: _LOGGER.debug( "Saving and storing energy cache for %s", self._mac_in_str ) await self.save_cache() + return result, slots_empty - return True + return result, slots_empty def _check_timestamp_is_recent( self, address: int, slot: int, timestamp: datetime @@ -726,48 +732,6 @@ async def _energy_log_records_save_to_cache(self) -> None: _LOGGER.debug("Updating in-memory energy log records for %s", self._mac_in_str) self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs) - async def _energy_log_record_update_state( - self, - address: int, - slot: int, - timestamp: datetime, - pulses: int, - import_only: bool = False, - ) -> bool: - """Process new energy log record. Returns true if record is new or changed.""" - self._energy_counters.add_pulse_log( - address, slot, timestamp, pulses, import_only=import_only - ) - if not self._cache_enabled: - return False - - log_cache_record = ( - f"{address}:{slot}:{timestamp.strftime('%Y-%m-%d-%H-%M-%S')}:{pulses}" - ) - if (cached_logs := self._get_cache(CACHE_ENERGY_COLLECTION)) is not None: - entries = cached_logs.split("|") if cached_logs else [] - if log_cache_record not in entries: - _LOGGER.debug( - "Adding logrecord (%s, %s) to cache of %s", - str(address), - str(slot), - self._mac_in_str, - ) - return True - - _LOGGER.debug( - "Energy logrecord already present for %s, ignoring", self._mac_in_str - ) - return False - - _LOGGER.debug( - "Cache is empty, adding new logrecord (%s, %s) for %s", - str(address), - str(slot), - self._mac_in_str, - ) - return True - @raise_not_loaded async def set_relay(self, state: bool) -> bool: """Change the state of the relay.""" From 4e752ea34be9f2fa76088bf45c36f94d9cb3d494 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 10:42:22 +0200 Subject: [PATCH 17/23] Fix error, improve, as suggested --- plugwise_usb/nodes/circle.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index f87dd62f1..b682df397 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -518,12 +518,12 @@ async def _get_initial_energy_logs(self) -> None: max_addresses_to_collect, ceil(datetime.now(tz=UTC).hour / factor) + 1 ) log_address = self._current_log_address + any_updates = False while total_addresses > 0: result, slots_empty = await self.energy_log_update( log_address, save_cache=False ) - if result and not slots_empty: - # (address with outdated data in slots is stored as with None data!) + if result and slots_empty: # Stop initial log collection when an address contains no (None) or outdated data # Outdated data can indicate a EnergyLog address rollover: from address 6014 to 0 _LOGGER.debug( @@ -532,10 +532,11 @@ async def _get_initial_energy_logs(self) -> None: ) break + any_updates |= (not slots_empty) log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 - if self._cache_enabled: + if self._cache_enabled and any_updates: await self.save_cache() async def get_missing_energy_logs(self) -> None: @@ -560,8 +561,9 @@ async def get_missing_energy_logs(self) -> None: create_task(self.energy_log_update(address, save_cache=False)) for address in missing_addresses ] + any_updates = False for idx, task in enumerate(tasks): - result = await task + result, slots_empty = await task # When an energy log collection task returns False, stop and cancel the remaining tasks if not result: to_cancel = tasks[idx + 1 :] @@ -571,7 +573,9 @@ async def get_missing_energy_logs(self) -> None: await gather(*to_cancel, return_exceptions=True) break - if self._cache_enabled: + any_updates |= (not slots_empty) + + if self._cache_enabled and any_updates: await self.save_cache() async def energy_log_update( From 73d5236fff327a9e9e786be5d3f59891a2b1cb12 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 10:53:39 +0200 Subject: [PATCH 18/23] Implement more suggestions --- plugwise_usb/nodes/circle.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index b682df397..11d7ad4ab 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -402,7 +402,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 return None # Always request the most recent energy log records at initial startup, check if the current - # address is acutally reported by the node even when all slots at that address are empty. + # address is actually reported by the node even when all slots at that address are empty. if not self._last_energy_log_requested: self._last_energy_log_requested, _ = await self.energy_log_update( self._current_log_address, save_cache=False @@ -418,7 +418,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 return None # Try collecting energy-stats from _current_log_address - result, _ = await self.energy_log_update( + result, slots_empty_cur = await self.energy_log_update( self._current_log_address, save_cache=False ) if not result: @@ -432,7 +432,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 # Retry with previous log address as Circle node pointer to self._current_log_address # could be rolled over while the last log is at previous address prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4) - result, _ = await self.energy_log_update(prev_log_address, save_cache=False) + result, slots_empty_prev = await self.energy_log_update(prev_log_address, save_cache=False) if not result: _LOGGER.debug( "async_energy_update | %s | Log rollover | energy_log_update from address %s failed", @@ -441,7 +441,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return None - if self._cache_enabled: + if self._cache_enabled and (not slots_empty_cur or not slots_empty_prev): await self.save_cache() if ( @@ -583,9 +583,9 @@ async def energy_log_update( ) -> tuple[bool, bool]: """Request energy logs from node and store them. - Return first bool as True if processing succeeded: records stored in memory, also with empty slots. - Return fist bool as False on transport or address errors. - Return second bool as False when all slots are empty otherwise as True + Return first bool as True if processing succeeded (records stored in memory, possibly all-empty). + Return first bool as False on transport or address errors. + Return second bool as False when all slots are empty or outdated; otherwise True. """ result = False slots_empty = True @@ -613,7 +613,6 @@ async def energy_log_update( # Each response message contains 4 log counters (slots) of the # energy pulses collected during the previous hour of given timestamp cache_updated = False - slot_updated = False for _slot in range(4, 0, -1): log_timestamp, log_pulses = response.log_data[_slot] _LOGGER.debug( @@ -631,18 +630,14 @@ async def energy_log_update( log_pulses, import_only=True, ) - slot_updated = True - - cache_updated |= slot_updated + cache_updated = True self._energy_counters.update() if cache_updated: slots_empty = False await self._energy_log_records_save_to_cache() if save_cache: - _LOGGER.debug( - "Saving and storing energy cache for %s", self._mac_in_str - ) + _LOGGER.debug("Saving energy cache for %s", self._mac_in_str) await self.save_cache() return result, slots_empty From 437df118af9805ec131e33be55a3d3bfe165d22c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 11:19:17 +0200 Subject: [PATCH 19/23] Fine tuning --- plugwise_usb/nodes/circle.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 11d7ad4ab..b94b49aab 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -583,9 +583,10 @@ async def energy_log_update( ) -> tuple[bool, bool]: """Request energy logs from node and store them. - Return first bool as True if processing succeeded (records stored in memory, possibly all-empty). - Return first bool as False on transport or address errors. - Return second bool as False when all slots are empty or outdated; otherwise True. + First bool: True when processing succeeded (records stored in memory, possibly all-empty); + False only on transport or address errors. + Second bool: slots_empty — True when all four slots at the address are empty or outdated; + False when at least one recent, non-empty record was stored. """ result = False slots_empty = True @@ -618,17 +619,13 @@ async def energy_log_update( _LOGGER.debug( "In slot=%s: pulses=%s, timestamp=%s", _slot, log_pulses, log_timestamp ) + address = response.log_address + log_timestamp = log_timestamp.replace(tzinfo=UTC) if log_timestamp is None or log_pulses is None: - self._energy_counters.add_empty_log(response.log_address, _slot) - elif self._check_timestamp_is_recent( - response.log_address, _slot, log_timestamp.replace(tzinfo=UTC) - ): + self._energy_counters.add_empty_log(address, _slot) + elif self._check_timestamp_is_recent(address, _slot, log_timestamp): self._energy_counters.add_pulse_log( - response.log_address, - _slot, - log_timestamp.replace(tzinfo=UTC), - log_pulses, - import_only=True, + address, _slot, log_timestamp, log_pulses, import_only=True, ) cache_updated = True From 159551c7279fd19a4ad0da8c626c76708a627e59 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 11:25:59 +0200 Subject: [PATCH 20/23] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2522a2198..ec605a838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Ongoing + +- PR [329](https://github.com/plugwise/python-plugwise-usb/pull/329): Improve EnergyLogs caching: store only data from MAX_LOG_HOURS (24) + ## v0.44.13 - 2025-08-29 - PR [327](https://github.com/plugwise/python-plugwise-usb/pull/327): Improve code quality according to SonarCloud, simplify sed awake timer From c86923aa646585aaf4404611450bbc745e6af90f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 11:26:21 +0200 Subject: [PATCH 21/23] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6284d8358..3587dc402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.14a2" +version = "0.44.14a3" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 16517a3500ca4646df4318fbcf61e1f5ab91e78d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 11:36:52 +0200 Subject: [PATCH 22/23] Fix --- plugwise_usb/nodes/circle.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index b94b49aab..98e93fdc9 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -432,7 +432,9 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 # Retry with previous log address as Circle node pointer to self._current_log_address # could be rolled over while the last log is at previous address prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4) - result, slots_empty_prev = await self.energy_log_update(prev_log_address, save_cache=False) + result, slots_empty_prev = await self.energy_log_update( + prev_log_address, save_cache=False + ) if not result: _LOGGER.debug( "async_energy_update | %s | Log rollover | energy_log_update from address %s failed", @@ -532,7 +534,7 @@ async def _get_initial_energy_logs(self) -> None: ) break - any_updates |= (not slots_empty) + any_updates |= not slots_empty log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 @@ -573,7 +575,7 @@ async def get_missing_energy_logs(self) -> None: await gather(*to_cancel, return_exceptions=True) break - any_updates |= (not slots_empty) + any_updates |= not slots_empty if self._cache_enabled and any_updates: await self.save_cache() @@ -620,13 +622,18 @@ async def energy_log_update( "In slot=%s: pulses=%s, timestamp=%s", _slot, log_pulses, log_timestamp ) address = response.log_address - log_timestamp = log_timestamp.replace(tzinfo=UTC) if log_timestamp is None or log_pulses is None: self._energy_counters.add_empty_log(address, _slot) - elif self._check_timestamp_is_recent(address, _slot, log_timestamp): - self._energy_counters.add_pulse_log( - address, _slot, log_timestamp, log_pulses, import_only=True, - ) + else: + log_timestamp = log_timestamp.replace(tzinfo=UTC) + if self._check_timestamp_is_recent(address, _slot, log_timestamp): + self._energy_counters.add_pulse_log( + address, + _slot, + log_timestamp, + log_pulses, + import_only=True, + ) cache_updated = True self._energy_counters.update() From 62753d0b047b3a6c662103c1fb9d0c0d3a602823 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 31 Aug 2025 12:14:06 +0200 Subject: [PATCH 23/23] Update CHANGELOG with release-version --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec605a838..b6d85d100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Ongoing +## 0.44.14 - 2025-08-31 - PR [329](https://github.com/plugwise/python-plugwise-usb/pull/329): Improve EnergyLogs caching: store only data from MAX_LOG_HOURS (24) diff --git a/pyproject.toml b/pyproject.toml index 3587dc402..4e9dc2287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.14a3" +version = "0.44.14" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [