From e85f040160401afc779033513c8f9ffc8a62bb48 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Jun 2025 10:18:15 +0200 Subject: [PATCH 01/74] Clear energy_collection from cache when resetting energy-logging --- plugwise_usb/network/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 8006dfa40..6a82774d9 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -14,6 +14,7 @@ from ..connection import StickController from ..constants import ENERGY_NODE_TYPES, UTF8 from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout +from ..nodes.helpers.cache import NodeCache from ..helpers.util import validate_mac from ..messages.requests import ( CircleClockSetRequest, @@ -33,6 +34,7 @@ from ..nodes import get_plugwise_node from .registry import StickNetworkRegister +CACHE_ENERGY_COLLECTION = "energy_collection" _LOGGER = logging.getLogger(__name__) # endregion @@ -543,6 +545,11 @@ async def energy_reset_request(self, mac: str) -> None: f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" ) + # Clear the cached energy_collection + if self._cache_enabled: + node_cache = NodeCache(mac, "") + node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + async def set_energy_intervals( self, mac: str, consumption: int, production: int ) -> None: From d887575245a73304b2e51c03022430ff129f0e58 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Jun 2025 16:17:03 +0200 Subject: [PATCH 02/74] Save cache from memory --- plugwise_usb/network/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 6a82774d9..a53158f17 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -549,6 +549,7 @@ async def energy_reset_request(self, mac: str) -> None: if self._cache_enabled: node_cache = NodeCache(mac, "") node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + await node_cache.save_cache() async def set_energy_intervals( self, mac: str, consumption: int, production: int From 349778f53e7752954eddf0147c8e70eced71d2fd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Jun 2025 16:23:41 +0200 Subject: [PATCH 03/74] Empty string not needed for NodeCache init --- plugwise_usb/network/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index a53158f17..e99ffdc04 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -547,7 +547,7 @@ async def energy_reset_request(self, mac: str) -> None: # Clear the cached energy_collection if self._cache_enabled: - node_cache = NodeCache(mac, "") + node_cache = NodeCache(mac) node_cache.update_state(CACHE_ENERGY_COLLECTION, "") await node_cache.save_cache() From 624ffa43fff6dd70dee3f8c874439e98b45db704 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Jun 2025 16:36:25 +0200 Subject: [PATCH 04/74] _energy_log_records_load_from_cache(): guard for empty log --- plugwise_usb/nodes/circle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index be90c9d0f..43ecf309b 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -527,6 +527,9 @@ async def _energy_log_records_load_from_cache(self) -> bool: return False restored_logs: dict[int, list[int]] = {} log_data = cache_data.split("|") + if len(log_data) == 0: + return False + for log_record in log_data: log_fields = log_record.split(":") if len(log_fields) == 4: @@ -558,6 +561,7 @@ async def _energy_log_records_load_from_cache(self) -> bool: # Create task to retrieve remaining (missing) logs if self._energy_counters.log_addresses_missing is None: return False + if len(self._energy_counters.log_addresses_missing) > 0: if self._retrieve_energy_logs_task is not None: if not self._retrieve_energy_logs_task.done(): @@ -566,6 +570,7 @@ async def _energy_log_records_load_from_cache(self) -> bool: self.get_missing_energy_logs() ) return False + return True async def _energy_log_records_save_to_cache(self) -> None: From dd1fb14121e3d256dee34af757fc681312afc98a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 11 Jun 2025 09:54:54 +0200 Subject: [PATCH 05/74] Reset PulseCollection._logs at energy_reset too --- plugwise_usb/network/__init__.py | 5 +++++ plugwise_usb/nodes/helpers/pulses.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index e99ffdc04..f1f59a20c 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -15,6 +15,7 @@ from ..constants import ENERGY_NODE_TYPES, UTF8 from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..nodes.helpers.cache import NodeCache +from ..nodes.helpers.pulses import PulseCollection from ..helpers.util import validate_mac from ..messages.requests import ( CircleClockSetRequest, @@ -551,6 +552,10 @@ async def energy_reset_request(self, mac: str) -> None: node_cache.update_state(CACHE_ENERGY_COLLECTION, "") await node_cache.save_cache() + # Clear PulseCollection._logs + pulse_collection = PulseCollection(mac) + pulse_collection.reset_logs() + async def set_energy_intervals( self, mac: str, consumption: int, production: int ) -> None: diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index dda31cd02..80f3a77dc 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -163,6 +163,10 @@ def pulse_counter_reset(self) -> bool: """Return a pulse_counter reset.""" return self._cons_pulsecounter_reset or self._prod_pulsecounter_reset + def reset_logs(self) -> None: + """Reset _logs() after e.g. an energy-logs reset.""" + self._logs = {} + def collected_pulses( self, from_timestamp: datetime, is_consumption: bool ) -> tuple[int | None, datetime | None]: From a27cb2ba26c94426c24ec00e0d1e45e4b9a2c6a6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 11 Jun 2025 10:23:24 +0200 Subject: [PATCH 06/74] Improve/add _logs_missing() debug logging --- plugwise_usb/nodes/helpers/pulses.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 80f3a77dc..bdd8dd2aa 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -858,7 +858,7 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: missing = [] _LOGGER.debug( - "_logs_missing | %s | first_address=%s, last_address=%s, from_timestamp=%s", + "_logs_missing | %s | checking in range: first_address=%s, last_address=%s, from_timestamp=%s", self._mac, first_address, last_address, @@ -930,6 +930,10 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: return None # We have an suspected interval, so try to calculate missing log addresses prior to first collected log + _LOGGER.debug( + "_logs_missing | %s | checking before range with log_interval=%s", + log_interval, + ) calculated_timestamp = self._logs[first_address][ first_slot ].timestamp - timedelta(minutes=log_interval) From 67312f588e2e8f7d039978b719608595d19a8946 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 11 Jun 2025 10:37:42 +0200 Subject: [PATCH 07/74] Fix logger-error, fix pylance-error --- plugwise_usb/nodes/helpers/pulses.py | 117 ++++++++++++++------------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index bdd8dd2aa..bc3a96f1c 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -517,8 +517,64 @@ def _update_log_direction( if self._logs is None: return - prev_timestamp = self._check_prev_production(address, slot, timestamp) - next_timestamp = self._check_next_production(address, slot, timestamp) + def check_prev_production( + self, address: int, slot: int, timestamp: datetime + ) -> datetime | None: + """Check the previous slot for production pulses.""" + prev_address, prev_slot = calc_log_address(address, slot, -1) + if self._log_exists(prev_address, prev_slot): + prev_timestamp = self._logs[prev_address][prev_slot].timestamp + if not self._first_prev_log_processed: + self._first_prev_log_processed = True + if prev_timestamp == timestamp: + # Given log is the second log with same timestamp, + # mark direction as production + self._logs[address][slot].is_consumption = False + self._logs[prev_address][prev_slot].is_consumption = True + self._log_production = True + elif self._log_production: + self._logs[address][slot].is_consumption = True + if self._logs[prev_address][prev_slot].is_consumption: + self._logs[prev_address][prev_slot].is_consumption = False + self._reset_log_references() + elif self._log_production is None: + self._log_production = False + return prev_timestamp + + if self._first_prev_log_processed: + self._first_prev_log_processed = False + return None + + def check_next_production( + self, address: int, slot: int, timestamp: datetime + ) -> datetime | None: + """Check the next slot for production pulses.""" + next_address, next_slot = calc_log_address(address, slot, 1) + if self._log_exists(next_address, next_slot): + next_timestamp = self._logs[next_address][next_slot].timestamp + if not self._first_next_log_processed: + self._first_next_log_processed = True + if next_timestamp == timestamp: + # Given log is the first log with same timestamp, + # mark direction as production of next log + self._logs[address][slot].is_consumption = True + if self._logs[next_address][next_slot].is_consumption: + self._logs[next_address][next_slot].is_consumption = False + self._reset_log_references() + self._log_production = True + elif self._log_production: + self._logs[address][slot].is_consumption = False + self._logs[next_address][next_slot].is_consumption = True + elif self._log_production is None: + self._log_production = False + return next_timestamp + + if self._first_next_log_processed: + self._first_next_log_processed = False + return None + + prev_timestamp = check_prev_production(self, address, slot, timestamp) + next_timestamp = check_next_production(self, address, slot, timestamp) if self._first_prev_log_processed and self._first_next_log_processed: # _log_production is True when 2 out of 3 consecutive slots have # the same timestamp @@ -526,62 +582,6 @@ def _update_log_direction( next_timestamp == timestamp ) - def _check_prev_production( - self, address: int, slot: int, timestamp: datetime - ) -> datetime | None: - """Check the previous slot for production pulses.""" - prev_address, prev_slot = calc_log_address(address, slot, -1) - if self._log_exists(prev_address, prev_slot): - prev_timestamp = self._logs[prev_address][prev_slot].timestamp - if not self._first_prev_log_processed: - self._first_prev_log_processed = True - if prev_timestamp == timestamp: - # Given log is the second log with same timestamp, - # mark direction as production - self._logs[address][slot].is_consumption = False - self._logs[prev_address][prev_slot].is_consumption = True - self._log_production = True - elif self._log_production: - self._logs[address][slot].is_consumption = True - if self._logs[prev_address][prev_slot].is_consumption: - self._logs[prev_address][prev_slot].is_consumption = False - self._reset_log_references() - elif self._log_production is None: - self._log_production = False - return prev_timestamp - - if self._first_prev_log_processed: - self._first_prev_log_processed = False - return None - - def _check_next_production( - self, address: int, slot: int, timestamp: datetime - ) -> datetime | None: - """Check the next slot for production pulses.""" - next_address, next_slot = calc_log_address(address, slot, 1) - if self._log_exists(next_address, next_slot): - next_timestamp = self._logs[next_address][next_slot].timestamp - if not self._first_next_log_processed: - self._first_next_log_processed = True - if next_timestamp == timestamp: - # Given log is the first log with same timestamp, - # mark direction as production of next log - self._logs[address][slot].is_consumption = True - if self._logs[next_address][next_slot].is_consumption: - self._logs[next_address][next_slot].is_consumption = False - self._reset_log_references() - self._log_production = True - elif self._log_production: - self._logs[address][slot].is_consumption = False - self._logs[next_address][next_slot].is_consumption = True - elif self._log_production is None: - self._log_production = False - return next_timestamp - - if self._first_next_log_processed: - self._first_next_log_processed = False - return None - def _update_log_interval(self) -> None: """Update the detected log interval based on the most recent two logs.""" if self._logs is None or self._log_production is None: @@ -932,6 +932,7 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: # We have an suspected interval, so try to calculate missing log addresses prior to first collected log _LOGGER.debug( "_logs_missing | %s | checking before range with log_interval=%s", + self._mac, log_interval, ) calculated_timestamp = self._logs[first_address][ From 74e96cf60ee4eace0a68dbc361ee02eb77415b72 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 11 Jun 2025 11:00:35 +0200 Subject: [PATCH 08/74] Add debug-log-message --- plugwise_usb/nodes/helpers/pulses.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index bc3a96f1c..e71754f71 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -413,6 +413,7 @@ def add_empty_log(self, address: int, slot: int) -> None: ): self._last_empty_log_slot = slot recalculate = True + if recalculate: self.recalculate_missing_log_addresses() @@ -938,14 +939,21 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: calculated_timestamp = self._logs[first_address][ first_slot ].timestamp - timedelta(minutes=log_interval) + _LOGGER.debug( + "_logs_missing | %s | first_empty_log_address=%s", + self._mac, + self._first_empty_log_address, + ) while from_timestamp < calculated_timestamp: if ( address == self._first_empty_log_address and slot == self._first_empty_log_slot ): break + if address not in missing: missing.append(address) + calculated_timestamp -= timedelta(minutes=log_interval) address, slot = calc_log_address(address, slot, -1) From 94e0f34a621dfc0f41345989a5e9adc3a7b61f62 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 11 Jun 2025 15:07:09 +0200 Subject: [PATCH 09/74] Reset the complete pulse collection --- plugwise_usb/network/__init__.py | 2 +- plugwise_usb/nodes/helpers/pulses.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index f1f59a20c..e1804372c 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -554,7 +554,7 @@ async def energy_reset_request(self, mac: str) -> None: # Clear PulseCollection._logs pulse_collection = PulseCollection(mac) - pulse_collection.reset_logs() + pulse_collection.reset() async def set_energy_intervals( self, mac: str, consumption: int, production: int diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index e71754f71..1c0dd214c 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -163,9 +163,10 @@ def pulse_counter_reset(self) -> bool: """Return a pulse_counter reset.""" return self._cons_pulsecounter_reset or self._prod_pulsecounter_reset - def reset_logs(self) -> None: - """Reset _logs() after e.g. an energy-logs reset.""" - self._logs = {} + def reset(self) -> None: + """Reset PulseCollection after e.g. an energy-logs reset.""" + mac = self._mac + self.__init__(mac) def collected_pulses( self, from_timestamp: datetime, is_consumption: bool From a64378ee5d8db40a03a0848f5b1d07ade709703d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 13:47:40 +0200 Subject: [PATCH 10/74] Move energy-reset function into circle.py --- plugwise_usb/network/__init__.py | 29 ----------------------------- plugwise_usb/nodes/circle.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index e1804372c..05363dafc 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -527,35 +527,6 @@ async def stop(self) -> None: # endregion - async def energy_reset_request(self, mac: str) -> None: - """Send an energy-reset to a Node.""" - self._validate_energy_node(mac) - node_protocols = self._nodes[mac].node_protocols - request = CircleClockSetRequest( - self._controller.send, - bytes(mac, UTF8), - datetime.now(tz=UTC), - node_protocols.max, - True, - ) - if (response := await request.send()) is None: - raise NodeError(f"Energy-reset for {mac} failed") - - if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: - raise MessageError( - f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" - ) - - # Clear the cached energy_collection - if self._cache_enabled: - node_cache = NodeCache(mac) - node_cache.update_state(CACHE_ENERGY_COLLECTION, "") - await node_cache.save_cache() - - # Clear PulseCollection._logs - pulse_collection = PulseCollection(mac) - pulse_collection.reset() - async def set_energy_intervals( self, mac: str, consumption: int, production: int ) -> None: diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 43ecf309b..732017369 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1208,3 +1208,29 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any states[NodeFeature.AVAILABLE] = self.available_state return states + + async def energy_reset_request(self) -> None: + """Send an energy-reset to a Node.""" + request = CircleClockSetRequest( + self._send, + self._mac_in_bytes, + datetime.now(tz=UTC), + self._node_protocols.max, + True, + ) + if (response := await request.send()) is None: + raise NodeError(f"Energy-reset for {mac} failed") + + if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: + raise MessageError( + f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" + ) + + # Clear the cached energy_collection + if self._cache_enabled: + self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + await self._node_cache.save_cache() + + # Request a NodeInfo update + if await self.node_info_update() is None: + _LOGGER.warning("Node info update failed after energy-reset") From d675e20f84b34f6c644a45d93d60db32a507a023 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 14:09:54 +0200 Subject: [PATCH 11/74] Reset pulse-collection and request a NodeInfo update --- plugwise_usb/nodes/circle.py | 18 +++++++++++++++--- plugwise_usb/nodes/helpers/counter.py | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 732017369..6bea58137 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -28,7 +28,7 @@ PULSES_PER_KW_SECOND, SECOND_IN_NANOSECONDS, ) -from ..exceptions import FeatureError, NodeError +from ..exceptions import FeatureError, MessageError, NodeError from ..messages.requests import ( CircleClockGetRequest, CircleClockSetRequest, @@ -43,7 +43,7 @@ from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters from .helpers.firmware import CIRCLE_FIRMWARE_SUPPORT -from .helpers.pulses import PulseLogRecord, calc_log_address +from .helpers.pulses import PulseCollection, PulseLogRecord, calc_log_address from .node import PlugwiseBaseNode CACHE_CURRENT_LOG_ADDRESS = "current_log_address" @@ -1211,6 +1211,11 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any async def energy_reset_request(self) -> None: """Send an energy-reset to a Node.""" + if self._node_protocols is None: + raise NodeError( + "Unable to energy-rest when protocol version is unknown" + ) + request = CircleClockSetRequest( self._send, self._mac_in_bytes, @@ -1219,18 +1224,25 @@ async def energy_reset_request(self) -> None: True, ) if (response := await request.send()) is None: - raise NodeError(f"Energy-reset for {mac} failed") + raise NodeError(f"Energy-reset for {self._mac_in_str} failed") if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: raise MessageError( f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" ) + _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_bytes) # Clear the cached energy_collection if self._cache_enabled: self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + _LOGGER.warning("Energy-collection cache cleared successfully") await self._node_cache.save_cache() + # Clear PulseCollection._logs + self._energy_counters.reset_pulse_collection() + # Request a NodeInfo update if await self.node_info_update() is None: _LOGGER.warning("Node info update failed after energy-reset") + else: + _LOGGER.warning("Node info update after energy-reset successful") diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index c55aa46bd..14bee2ee1 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -103,6 +103,10 @@ def add_pulse_stats( ) self.update() + def reset_pulse_collection(self) -> None: + """ Reset the related pulse collection.""" + self._pulse_collection.reset() + @property def energy_statistics(self) -> EnergyStatistics: """Return collection with energy statistics.""" From 017dc799ca1a75ecd7b2b4bc5df47a1615867024 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 14:42:30 +0200 Subject: [PATCH 12/74] Fix pylint issue --- plugwise_usb/__init__.py | 17 -------------- plugwise_usb/nodes/circle.py | 33 ++++++++++++++++++++++++++-- plugwise_usb/nodes/helpers/pulses.py | 6 ++--- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 2061ca32c..e3cc2c6b3 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -14,7 +14,6 @@ from .api import NodeEvent, PlugwiseNode, StickEvent from .connection import StickController -from .constants import DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL from .exceptions import MessageError, NodeError, StickError, SubscriptionError from .network import StickNetwork @@ -177,22 +176,6 @@ def port(self, port: str) -> None: self._port = port - async def energy_reset_request(self, mac: str) -> bool: - """Send an energy-reset request to a Node.""" - _LOGGER.debug("Resetting energy logs for %s", mac) - try: - await self._network.energy_reset_request(mac) - except (MessageError, NodeError) as exc: - raise NodeError(f"{exc}") from exc - - # Follow up by an energy-intervals (re)set - if result := await self.set_energy_intervals( - mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL - ): - return result - - return False - async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 6bea58137..e1d8152f3 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -23,8 +23,10 @@ ) from ..connection import StickController from ..constants import ( + DEFAULT_CONS_INTERVAL, MAX_TIME_DRIFT, MINIMAL_POWER_UPDATE, + NO_PRODUCTION_INTERVAL, PULSES_PER_KW_SECOND, SECOND_IN_NANOSECONDS, ) @@ -42,8 +44,7 @@ from ..messages.responses import NodeInfoResponse, NodeResponseType from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters -from .helpers.firmware import CIRCLE_FIRMWARE_SUPPORT -from .helpers.pulses import PulseCollection, PulseLogRecord, calc_log_address +from .network import StickNetwork from .node import PlugwiseBaseNode CACHE_CURRENT_LOG_ADDRESS = "current_log_address" @@ -1232,6 +1233,15 @@ async def energy_reset_request(self) -> None: ) _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_bytes) + # Follow up by an energy-intervals (re)set + network = StickNetwork() + if not ( + result := await network.set_energy_intervals( + mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL + ) + ): + _LOGGER.warning("Failed enery-intervals (re)set after an energy-reset") + # Clear the cached energy_collection if self._cache_enabled: self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") @@ -1246,3 +1256,22 @@ async def energy_reset_request(self) -> None: _LOGGER.warning("Node info update failed after energy-reset") else: _LOGGER.warning("Node info update after energy-reset successful") + + + async def energy_reset_request(self, mac: str) -> bool: + """Send an energy-reset request to a Node.""" + _LOGGER.debug("Resetting energy logs for %s", mac) + try: + await self._network.energy_reset_request(mac) + except (MessageError, NodeError) as exc: + raise NodeError(f"{exc}") from exc + + # Follow up by an energy-intervals (re)set + if ( + result := await self.set_energy_intervals( + mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL + ) + ): + return result + + return False \ No newline at end of file diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 1c0dd214c..cbcfc3aa0 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -164,9 +164,9 @@ def pulse_counter_reset(self) -> bool: return self._cons_pulsecounter_reset or self._prod_pulsecounter_reset def reset(self) -> None: - """Reset PulseCollection after e.g. an energy-logs reset.""" - mac = self._mac - self.__init__(mac) + """Reset PulseCollection after an energy-logs reset.""" + # Keep mac, wipe every other attribute. + self.__dict__.update(PulseCollection(self._mac).__dict__) def collected_pulses( self, from_timestamp: datetime, is_consumption: bool From 5edaf68f146d82e44f308a80a0d987f7523dc21d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 14:48:15 +0200 Subject: [PATCH 13/74] Implement improved guarding as suggested --- 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 e1d8152f3..04a74b8dd 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -527,10 +527,10 @@ async def _energy_log_records_load_from_cache(self) -> bool: ) return False restored_logs: dict[int, list[int]] = {} - log_data = cache_data.split("|") - if len(log_data) == 0: + if cache_data == "": return False + log_data = cache_data.split("|") for log_record in log_data: log_fields = log_record.split(":") if len(log_fields) == 4: From 16e2a122a10b327dc6e132c3364c40cb6a6fc8c1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 14:53:05 +0200 Subject: [PATCH 14/74] Fix network import --- 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 04a74b8dd..a3bb3b03c 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -42,9 +42,9 @@ NodeInfoRequest, ) from ..messages.responses import NodeInfoResponse, NodeResponseType +from ..network import StickNetwork from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters -from .network import StickNetwork from .node import PlugwiseBaseNode CACHE_CURRENT_LOG_ADDRESS = "current_log_address" From b16a0f10c1123a989ccc517ebc082d69df08c7f2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 14:54:24 +0200 Subject: [PATCH 15/74] Remove unused --- 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 a3bb3b03c..1ad6e0880 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1236,7 +1236,7 @@ async def energy_reset_request(self) -> None: # Follow up by an energy-intervals (re)set network = StickNetwork() if not ( - result := await network.set_energy_intervals( + await network.set_energy_intervals( mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL ) ): From 336e998d01a94d82ffe82cb2516af06713885415 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 15:05:32 +0200 Subject: [PATCH 16/74] Revert pulses-change and improve --- plugwise_usb/nodes/helpers/pulses.py | 125 ++++++++++++++------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index cbcfc3aa0..ebfa9b3c5 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -516,67 +516,8 @@ def _update_log_direction( or double slots containing consumption and production data. Single slots containing production data only is NOT supported/tested. """ - if self._logs is None: - return - - def check_prev_production( - self, address: int, slot: int, timestamp: datetime - ) -> datetime | None: - """Check the previous slot for production pulses.""" - prev_address, prev_slot = calc_log_address(address, slot, -1) - if self._log_exists(prev_address, prev_slot): - prev_timestamp = self._logs[prev_address][prev_slot].timestamp - if not self._first_prev_log_processed: - self._first_prev_log_processed = True - if prev_timestamp == timestamp: - # Given log is the second log with same timestamp, - # mark direction as production - self._logs[address][slot].is_consumption = False - self._logs[prev_address][prev_slot].is_consumption = True - self._log_production = True - elif self._log_production: - self._logs[address][slot].is_consumption = True - if self._logs[prev_address][prev_slot].is_consumption: - self._logs[prev_address][prev_slot].is_consumption = False - self._reset_log_references() - elif self._log_production is None: - self._log_production = False - return prev_timestamp - - if self._first_prev_log_processed: - self._first_prev_log_processed = False - return None - - def check_next_production( - self, address: int, slot: int, timestamp: datetime - ) -> datetime | None: - """Check the next slot for production pulses.""" - next_address, next_slot = calc_log_address(address, slot, 1) - if self._log_exists(next_address, next_slot): - next_timestamp = self._logs[next_address][next_slot].timestamp - if not self._first_next_log_processed: - self._first_next_log_processed = True - if next_timestamp == timestamp: - # Given log is the first log with same timestamp, - # mark direction as production of next log - self._logs[address][slot].is_consumption = True - if self._logs[next_address][next_slot].is_consumption: - self._logs[next_address][next_slot].is_consumption = False - self._reset_log_references() - self._log_production = True - elif self._log_production: - self._logs[address][slot].is_consumption = False - self._logs[next_address][next_slot].is_consumption = True - elif self._log_production is None: - self._log_production = False - return next_timestamp - - if self._first_next_log_processed: - self._first_next_log_processed = False - return None - - prev_timestamp = check_prev_production(self, address, slot, timestamp) - next_timestamp = check_next_production(self, address, slot, timestamp) + prev_timestamp = self._check_prev_production(self, address, slot, timestamp) + next_timestamp = self._check_next_production(self, address, slot, timestamp) if self._first_prev_log_processed and self._first_next_log_processed: # _log_production is True when 2 out of 3 consecutive slots have # the same timestamp @@ -584,6 +525,68 @@ def check_next_production( next_timestamp == timestamp ) + def _check_prev_production( + self, address: int, slot: int, timestamp: datetime + ) -> datetime | None: + """Check the previous slot for production pulses.""" + if self._logs is None: + return + + prev_address, prev_slot = calc_log_address(address, slot, -1) + if self._log_exists(prev_address, prev_slot): + prev_timestamp = self._logs[prev_address][prev_slot].timestamp + if not self._first_prev_log_processed: + self._first_prev_log_processed = True + if prev_timestamp == timestamp: + # Given log is the second log with same timestamp, + # mark direction as production + self._logs[address][slot].is_consumption = False + self._logs[prev_address][prev_slot].is_consumption = True + self._log_production = True + elif self._log_production: + self._logs[address][slot].is_consumption = True + if self._logs[prev_address][prev_slot].is_consumption: + self._logs[prev_address][prev_slot].is_consumption = False + self._reset_log_references() + elif self._log_production is None: + self._log_production = False + return prev_timestamp + + if self._first_prev_log_processed: + self._first_prev_log_processed = False + return None + + def _check_next_production( + self, address: int, slot: int, timestamp: datetime + ) -> datetime | None: + """Check the next slot for production pulses.""" + if self._logs is None: + return + + next_address, next_slot = calc_log_address(address, slot, 1) + if self._log_exists(next_address, next_slot): + next_timestamp = self._logs[next_address][next_slot].timestamp + if not self._first_next_log_processed: + self._first_next_log_processed = True + if next_timestamp == timestamp: + # Given log is the first log with same timestamp, + # mark direction as production of next log + self._logs[address][slot].is_consumption = True + if self._logs[next_address][next_slot].is_consumption: + self._logs[next_address][next_slot].is_consumption = False + self._reset_log_references() + self._log_production = True + elif self._log_production: + self._logs[address][slot].is_consumption = False + self._logs[next_address][next_slot].is_consumption = True + elif self._log_production is None: + self._log_production = False + return next_timestamp + + if self._first_next_log_processed: + self._first_next_log_processed = False + return None + def _update_log_interval(self) -> None: """Update the detected log interval based on the most recent two logs.""" if self._logs is None or self._log_production is None: From b0535e948d893bc8e583afbc76ec1e3b0e2b0ba9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 15:08:35 +0200 Subject: [PATCH 17/74] Revert imports deletion --- plugwise_usb/nodes/circle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 1ad6e0880..a14f43e90 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -45,6 +45,8 @@ from ..network import StickNetwork from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters +from .helpers.firmware import CIRCLE_FIRMWARE_SUPPORT +from .helpers.pulses import PulseLogRecord, calc_log_address from .node import PlugwiseBaseNode CACHE_CURRENT_LOG_ADDRESS = "current_log_address" From 170aff6b20485ca3344b9f0e13b5efc4ea33eddc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 15:11:20 +0200 Subject: [PATCH 18/74] Fix --- plugwise_usb/nodes/helpers/pulses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index ebfa9b3c5..12ce67f58 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -516,8 +516,8 @@ def _update_log_direction( or double slots containing consumption and production data. Single slots containing production data only is NOT supported/tested. """ - prev_timestamp = self._check_prev_production(self, address, slot, timestamp) - next_timestamp = self._check_next_production(self, address, slot, timestamp) + prev_timestamp = self._check_prev_production(address, slot, timestamp) + next_timestamp = self._check_next_production(address, slot, timestamp) if self._first_prev_log_processed and self._first_next_log_processed: # _log_production is True when 2 out of 3 consecutive slots have # the same timestamp From 74668e0c828a008db7fe63a0ed5aa9ca8f9fccb4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 15:26:04 +0200 Subject: [PATCH 19/74] Improve --- plugwise_usb/nodes/circle.py | 39 ++++++++++++------------------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index a14f43e90..2f9ba181f 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -35,6 +35,7 @@ CircleClockGetRequest, CircleClockSetRequest, CircleEnergyLogsRequest, + CircleMeasureIntervalRequest, CirclePowerUsageRequest, CircleRelayInitStateRequest, CircleRelaySwitchRequest, @@ -42,7 +43,6 @@ NodeInfoRequest, ) from ..messages.responses import NodeInfoResponse, NodeResponseType -from ..network import StickNetwork from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters from .helpers.firmware import CIRCLE_FIRMWARE_SUPPORT @@ -1236,13 +1236,19 @@ async def energy_reset_request(self) -> None: _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_bytes) # Follow up by an energy-intervals (re)set - network = StickNetwork() - if not ( - await network.set_energy_intervals( - mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL + request = CircleMeasureIntervalRequest( + self._send, + self._mac_in_bytes, + DEFAULT_CONS_INTERVAL, + NO_PRODUCTION_INTERVAL, + ) + if (response := await request.send()) is None: + raise NodeError("No response for CircleMeasureIntervalRequest.") + + if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: + raise MessageError( + f"Unknown NodeResponseType '{response.response_type.name}' received" ) - ): - _LOGGER.warning("Failed enery-intervals (re)set after an energy-reset") # Clear the cached energy_collection if self._cache_enabled: @@ -1258,22 +1264,3 @@ async def energy_reset_request(self) -> None: _LOGGER.warning("Node info update failed after energy-reset") else: _LOGGER.warning("Node info update after energy-reset successful") - - - async def energy_reset_request(self, mac: str) -> bool: - """Send an energy-reset request to a Node.""" - _LOGGER.debug("Resetting energy logs for %s", mac) - try: - await self._network.energy_reset_request(mac) - except (MessageError, NodeError) as exc: - raise NodeError(f"{exc}") from exc - - # Follow up by an energy-intervals (re)set - if ( - result := await self.set_energy_intervals( - mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL - ) - ): - return result - - return False \ No newline at end of file From 7619ac2cd5459448459bd71eb3142522ade2f37a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 15:37:15 +0200 Subject: [PATCH 20/74] Bump to v0.44.4a0 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0a9ec6544..33dcfdef7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.3" +version = "0.44.4a0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From e32af3c791225b6b8a8f002b99bfd7a81cc2c79a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 17:53:55 +0200 Subject: [PATCH 21/74] Add NodeFeature.CIRCLE --- plugwise_usb/api.py | 4 +++- plugwise_usb/nodes/circle.py | 2 ++ plugwise_usb/nodes/helpers/firmware.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 4f1140f4e..528617978 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -41,6 +41,7 @@ class NodeFeature(str, Enum): AVAILABLE = "available" BATTERY = "battery" + CIRCLE = "circle" CIRCLEPLUS = "circleplus" ENERGY = "energy" HUMIDITY = "humidity" @@ -78,13 +79,14 @@ class NodeType(Enum): PUSHING_FEATURES = ( NodeFeature.AVAILABLE, NodeFeature.BATTERY, + NodeFeature.CIRCLE, + NodeFeature.CIRCLEPLUS, NodeFeature.HUMIDITY, NodeFeature.MOTION, NodeFeature.MOTION_CONFIG, NodeFeature.TEMPERATURE, NodeFeature.SENSE, NodeFeature.SWITCH, - NodeFeature.CIRCLEPLUS, ) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 2f9ba181f..a0a0ff16b 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -800,6 +800,7 @@ async def load(self) -> bool: self._setup_protocol( CIRCLE_FIRMWARE_SUPPORT, ( + NodeFeature.CIRCLE, NodeFeature.RELAY, NodeFeature.RELAY_INIT, NodeFeature.RELAY_LOCK, @@ -841,6 +842,7 @@ async def load(self) -> bool: self._setup_protocol( CIRCLE_FIRMWARE_SUPPORT, ( + NodeFeature.CIRCLE, NodeFeature.RELAY, NodeFeature.RELAY_INIT, NodeFeature.RELAY_LOCK, diff --git a/plugwise_usb/nodes/helpers/firmware.py b/plugwise_usb/nodes/helpers/firmware.py index c2df3b4d0..1379b04c6 100644 --- a/plugwise_usb/nodes/helpers/firmware.py +++ b/plugwise_usb/nodes/helpers/firmware.py @@ -155,6 +155,8 @@ class SupportedVersions(NamedTuple): FEATURE_SUPPORTED_AT_FIRMWARE: Final = { NodeFeature.BATTERY: 2.0, + NodeFeature.CIRCLE: 2.0, + NodeFeature.CIRCLEPLUS: 2.0, NodeFeature.INFO: 2.0, NodeFeature.SENSE: 2.0, NodeFeature.TEMPERATURE: 2.0, @@ -167,7 +169,6 @@ class SupportedVersions(NamedTuple): NodeFeature.MOTION: 2.0, NodeFeature.MOTION_CONFIG: 2.0, NodeFeature.SWITCH: 2.0, - NodeFeature.CIRCLEPLUS: 2.0, } # endregion From 2dd323cd587fce393bcebf19840c661cd52ef66c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 18:03:09 +0200 Subject: [PATCH 22/74] Implement reset-improvement suggestion --- plugwise_usb/nodes/helpers/pulses.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 12ce67f58..020433ec1 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -166,7 +166,9 @@ def pulse_counter_reset(self) -> bool: def reset(self) -> None: """Reset PulseCollection after an energy-logs reset.""" # Keep mac, wipe every other attribute. - self.__dict__.update(PulseCollection(self._mac).__dict__) + fresh_state = PulseCollection(self._mac).__dict__ + self.__dict__.clear() # remove *all* existing keys first + self.__dict__.update(fresh_state) def collected_pulses( self, from_timestamp: datetime, is_consumption: bool From 4070c5d542fff09d9fa38620f0c3cbe59534da6f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 18:05:04 +0200 Subject: [PATCH 23/74] Update related test-asserts --- tests/test_usb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 831ed4e83..b94f94f96 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2488,12 +2488,12 @@ async def test_node_discovery_and_load( # noqa: PLR0915 assert sorted(state[pw_api.NodeFeature.INFO].features) == sorted( ( pw_api.NodeFeature.AVAILABLE, + pw_api.NodeFeature.CIRCLEPLUS, pw_api.NodeFeature.INFO, pw_api.NodeFeature.PING, pw_api.NodeFeature.RELAY, pw_api.NodeFeature.RELAY_LOCK, pw_api.NodeFeature.ENERGY, - pw_api.NodeFeature.CIRCLEPLUS, pw_api.NodeFeature.POWER, ) ) @@ -2540,6 +2540,7 @@ async def test_node_discovery_and_load( # noqa: PLR0915 assert sorted(state[pw_api.NodeFeature.INFO].features) == sorted( ( pw_api.NodeFeature.AVAILABLE, + pw_api.NodeFeature.CIRCLE, pw_api.NodeFeature.INFO, pw_api.NodeFeature.PING, pw_api.NodeFeature.RELAY, From e0ad70567da9c7a1b2293187c71a8d34f6e168d3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 18:30:59 +0200 Subject: [PATCH 24/74] Comment-out for testing --- plugwise_usb/nodes/circle.py | 62 +++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index a0a0ff16b..80bb1c9d6 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1221,45 +1221,47 @@ async def energy_reset_request(self) -> None: "Unable to energy-rest when protocol version is unknown" ) - request = CircleClockSetRequest( - self._send, - self._mac_in_bytes, - datetime.now(tz=UTC), - self._node_protocols.max, - True, - ) - if (response := await request.send()) is None: - raise NodeError(f"Energy-reset for {self._mac_in_str} failed") - - if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: - raise MessageError( - f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" - ) + #request = CircleClockSetRequest( + # self._send, + # self._mac_in_bytes, + # datetime.now(tz=UTC), + # self._node_protocols.max, + # True, + #) + #if (response := await request.send()) is None: + # raise NodeError(f"Energy-reset for {self._mac_in_str} failed") + + #if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: + # raise MessageError( + # f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" + # ) _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_bytes) # Follow up by an energy-intervals (re)set - request = CircleMeasureIntervalRequest( - self._send, - self._mac_in_bytes, - DEFAULT_CONS_INTERVAL, - NO_PRODUCTION_INTERVAL, - ) - if (response := await request.send()) is None: - raise NodeError("No response for CircleMeasureIntervalRequest.") - - if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: - raise MessageError( - f"Unknown NodeResponseType '{response.response_type.name}' received" - ) + #request = CircleMeasureIntervalRequest( + # self._send, + # self._mac_in_bytes, + # DEFAULT_CONS_INTERVAL, + # NO_PRODUCTION_INTERVAL, + #) + #if (response := await request.send()) is None: + # raise NodeError("No response for CircleMeasureIntervalRequest.") + + #if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: + # raise MessageError( + # f"Unknown NodeResponseType '{response.response_type.name}' received" + # ) + _LOGGER.warning("Resetting energy intervals to default (= consumption only)") # Clear the cached energy_collection if self._cache_enabled: - self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + #self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") _LOGGER.warning("Energy-collection cache cleared successfully") - await self._node_cache.save_cache() + #await self._node_cache.save_cache() # Clear PulseCollection._logs - self._energy_counters.reset_pulse_collection() + #self._energy_counters.reset_pulse_collection() + _LOGGER.warning("Resetting pulse-collection") # Request a NodeInfo update if await self.node_info_update() is None: From 2f2bafaadbc84aca49fd626b283248f52eae3ae2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 18:31:20 +0200 Subject: [PATCH 25/74] Bump to a1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 33dcfdef7..7ff822399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a0" +version = "0.44.4a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 6b147564d68134b276a9eb44e371c76ab484a44c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 18:38:22 +0200 Subject: [PATCH 26/74] Comment-out unused imports --- plugwise_usb/nodes/circle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 80bb1c9d6..2ef5e55dc 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -23,19 +23,19 @@ ) from ..connection import StickController from ..constants import ( - DEFAULT_CONS_INTERVAL, + # DEFAULT_CONS_INTERVAL, MAX_TIME_DRIFT, MINIMAL_POWER_UPDATE, - NO_PRODUCTION_INTERVAL, + # NO_PRODUCTION_INTERVAL, PULSES_PER_KW_SECOND, SECOND_IN_NANOSECONDS, ) -from ..exceptions import FeatureError, MessageError, NodeError +from ..exceptions import FeatureError, NodeError # MessageError from ..messages.requests import ( CircleClockGetRequest, CircleClockSetRequest, CircleEnergyLogsRequest, - CircleMeasureIntervalRequest, + # CircleMeasureIntervalRequest, CirclePowerUsageRequest, CircleRelayInitStateRequest, CircleRelaySwitchRequest, From 6fcd49c53f068b19305f5aa982b21d7d6801355a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 19:05:38 +0200 Subject: [PATCH 27/74] Add debug-logging to _energy_log_records_load_from_cache() --- plugwise_usb/nodes/circle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 2ef5e55dc..ce3b52c90 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -530,6 +530,7 @@ async def _energy_log_records_load_from_cache(self) -> bool: return False restored_logs: dict[int, list[int]] = {} if cache_data == "": + _LOGGER.debug("Cache-record is empty") return False log_data = cache_data.split("|") @@ -563,6 +564,7 @@ async def _energy_log_records_load_from_cache(self) -> bool: # Create task to retrieve remaining (missing) logs if self._energy_counters.log_addresses_missing is None: + _LOGGER.debug("Cache | missing log addresses is None") return False if len(self._energy_counters.log_addresses_missing) > 0: @@ -572,6 +574,7 @@ async def _energy_log_records_load_from_cache(self) -> bool: self._retrieve_energy_logs_task = create_task( self.get_missing_energy_logs() ) + _LOGGER.debug("Cache | creating tasks to obtain missing energy logs") return False return True From c28d7b52f975cbde16ad58f4f7cb1edcbe15f189 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 19:07:26 +0200 Subject: [PATCH 28/74] Fully enable energy-resetting --- plugwise_usb/nodes/circle.py | 69 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index ce3b52c90..3ce343243 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -23,19 +23,19 @@ ) from ..connection import StickController from ..constants import ( - # DEFAULT_CONS_INTERVAL, + DEFAULT_CONS_INTERVAL, MAX_TIME_DRIFT, MINIMAL_POWER_UPDATE, - # NO_PRODUCTION_INTERVAL, + NO_PRODUCTION_INTERVAL, PULSES_PER_KW_SECOND, SECOND_IN_NANOSECONDS, ) -from ..exceptions import FeatureError, NodeError # MessageError +from ..exceptions import FeatureError, MessageError, NodeError from ..messages.requests import ( CircleClockGetRequest, CircleClockSetRequest, CircleEnergyLogsRequest, - # CircleMeasureIntervalRequest, + CircleMeasureIntervalRequest, CirclePowerUsageRequest, CircleRelayInitStateRequest, CircleRelaySwitchRequest, @@ -1224,46 +1224,47 @@ async def energy_reset_request(self) -> None: "Unable to energy-rest when protocol version is unknown" ) - #request = CircleClockSetRequest( - # self._send, - # self._mac_in_bytes, - # datetime.now(tz=UTC), - # self._node_protocols.max, - # True, - #) - #if (response := await request.send()) is None: - # raise NodeError(f"Energy-reset for {self._mac_in_str} failed") - - #if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: - # raise MessageError( - # f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" - # ) + request = CircleClockSetRequest( + self._send, + self._mac_in_bytes, + datetime.now(tz=UTC), + self._node_protocols.max, + True, + ) + if (response := await request.send()) is None: + raise NodeError(f"Energy-reset for {self._mac_in_str} failed") + + if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: + raise MessageError( + f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" + ) _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_bytes) + # Follow up by an energy-intervals (re)set - #request = CircleMeasureIntervalRequest( - # self._send, - # self._mac_in_bytes, - # DEFAULT_CONS_INTERVAL, - # NO_PRODUCTION_INTERVAL, - #) - #if (response := await request.send()) is None: - # raise NodeError("No response for CircleMeasureIntervalRequest.") - - #if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: - # raise MessageError( - # f"Unknown NodeResponseType '{response.response_type.name}' received" - # ) + request = CircleMeasureIntervalRequest( + self._send, + self._mac_in_bytes, + DEFAULT_CONS_INTERVAL, + NO_PRODUCTION_INTERVAL, + ) + if (response := await request.send()) is None: + raise NodeError("No response for CircleMeasureIntervalRequest.") + + if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: + raise MessageError( + f"Unknown NodeResponseType '{response.response_type.name}' received" + ) _LOGGER.warning("Resetting energy intervals to default (= consumption only)") # Clear the cached energy_collection if self._cache_enabled: - #self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") _LOGGER.warning("Energy-collection cache cleared successfully") - #await self._node_cache.save_cache() + await self._node_cache.save_cache() # Clear PulseCollection._logs - #self._energy_counters.reset_pulse_collection() + self._energy_counters.reset_pulse_collection() _LOGGER.warning("Resetting pulse-collection") # Request a NodeInfo update From d57918761ea4fa76326e090daa52212d8437061a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 19:07:49 +0200 Subject: [PATCH 29/74] Bump to a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7ff822399..9592432b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a1" +version = "0.44.4a2" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 8e70dc9242fc2914769b341fa5c67a0fd220e0a3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Jun 2025 19:32:16 +0200 Subject: [PATCH 30/74] Remove blank spaces --- plugwise_usb/nodes/helpers/pulses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 020433ec1..2db5aa56f 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -557,7 +557,7 @@ def _check_prev_production( if self._first_prev_log_processed: self._first_prev_log_processed = False return None - + def _check_next_production( self, address: int, slot: int, timestamp: datetime ) -> datetime | None: From fcdf64aae06c0aa18798ae65a6b375408904e3ba Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Jun 2025 12:52:22 +0200 Subject: [PATCH 31/74] Add detection of collecting outdated energy data --- plugwise_usb/nodes/circle.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 3ce343243..403a509b2 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -101,6 +101,7 @@ def __init__( self._energy_counters = EnergyCounters(mac) self._retrieve_energy_logs_task: None | Task[None] = None self._last_energy_log_requested: bool = False + self._last_collected_energy_timestamp: datetime | None = None self._group_member: list[int] = [] @@ -441,13 +442,18 @@ async def get_missing_energy_logs(self) -> None: if (missing_addresses := self._energy_counters.log_addresses_missing) is None: _LOGGER.debug( - "Start with initial energy request for the last 10 log addresses for node %s.", + "Start with initial energy requests for the last 10 log addresses for node %s.", self._mac_in_str, ) total_addresses = 11 log_address = self._current_log_address while total_addresses > 0: await self.energy_log_update(log_address) + if ( + datetime.now(tz=UTC) - self._last_collected_energy_timestamp + ).total_seconds // 60 > 65: # assuming log_interval is 60 minutes + _LOGGER.debug("Energy data collected is outdated, stopping collection") + break log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 @@ -499,6 +505,7 @@ async def energy_log_update(self, address: int | None) -> bool: # Forward historical energy log information to energy counters # Each response message contains 4 log counters (slots) of the # energy pulses collected during the previous hour of given timestamp + last_energy_timestamp_collected = False for _slot in range(4, 0, -1): log_timestamp, log_pulses = response.log_data[_slot] _LOGGER.debug( @@ -514,6 +521,11 @@ async def energy_log_update(self, address: int | None) -> bool: import_only=True, ): energy_record_update = True + if not last_energy_timestamp_collected: + self._last_collected_energy_timestamp = ( + log_timestamp.replace(tzinfo=UTC) + ) + last_energy_timestamp_collected = True self._energy_counters.update() if energy_record_update: From 60700ab460fa12cd7837675c0e8f88b8feb5cd7c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Jun 2025 13:09:56 +0200 Subject: [PATCH 32/74] Remove caching of current_log_addres --- plugwise_usb/nodes/circle.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 403a509b2..312fa226f 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -49,7 +49,6 @@ from .helpers.pulses import PulseLogRecord, calc_log_address from .node import PlugwiseBaseNode -CACHE_CURRENT_LOG_ADDRESS = "current_log_address" CACHE_CALIBRATION_GAIN_A = "calibration_gain_a" CACHE_CALIBRATION_GAIN_B = "calibration_gain_b" CACHE_CALIBRATION_NOISE = "calibration_noise" @@ -973,16 +972,6 @@ async def node_info_update( await self._relay_update_state( node_info.relay_state, timestamp=node_info.timestamp ) - if ( - self._get_cache(CACHE_CURRENT_LOG_ADDRESS) is None - and node_info.current_logaddress_pointer - ): - self._set_cache( - CACHE_CURRENT_LOG_ADDRESS, - node_info.current_logaddress_pointer, - ) - await self.save_cache() - if self._current_log_address is not None and ( self._current_log_address > node_info.current_logaddress_pointer or self._current_log_address == 1 @@ -997,28 +986,9 @@ async def node_info_update( if self._current_log_address != node_info.current_logaddress_pointer: self._current_log_address = node_info.current_logaddress_pointer - self._set_cache( - CACHE_CURRENT_LOG_ADDRESS, node_info.current_logaddress_pointer - ) - await self.save_cache() return self._node_info - async def _node_info_load_from_cache(self) -> bool: - """Load node info settings from cache.""" - if ( - current_log_address := self._get_cache(CACHE_CURRENT_LOG_ADDRESS) - ) is not None: - self._current_log_address = int(current_log_address) - _LOGGER.debug( - "circle._node_info_load_from_cache | current_log_address=%s", - self._current_log_address, - ) - return True - - _LOGGER.debug("circle._node_info_load_from_cache | current_log_address=None") - return False - # pylint: disable=too-many-arguments async def update_node_details( self, node_info: NodeInfoResponse | None = None From 822527df04d836eeabbcc54e993e7fb9089ae747 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Jun 2025 13:20:05 +0200 Subject: [PATCH 33/74] Add guarding, as suggested --- 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 312fa226f..89f3b188c 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -448,7 +448,7 @@ async def get_missing_energy_logs(self) -> None: log_address = self._current_log_address while total_addresses > 0: await self.energy_log_update(log_address) - if ( + if self._last_collected_energy_timestamp is not None and ( datetime.now(tz=UTC) - self._last_collected_energy_timestamp ).total_seconds // 60 > 65: # assuming log_interval is 60 minutes _LOGGER.debug("Energy data collected is outdated, stopping collection") From 9497e2a3b9fc811fb5af5070441f290d150004e5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Jun 2025 14:49:47 +0200 Subject: [PATCH 34/74] Improve logic, the first collection is from the current_log_address --- plugwise_usb/nodes/circle.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 89f3b188c..88da02afa 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -446,13 +446,24 @@ async def get_missing_energy_logs(self) -> None: ) total_addresses = 11 log_address = self._current_log_address + prev_address_timestamp: datetime | None = None while total_addresses > 0: await self.energy_log_update(log_address) - if self._last_collected_energy_timestamp is not None and ( - datetime.now(tz=UTC) - self._last_collected_energy_timestamp - ).total_seconds // 60 > 65: # assuming log_interval is 60 minutes + # Check if the most recent timestamp of an earlier address is recent (4 * log_interval plus 5 mins margin) + if ( + log_address != self._current_log_address + and self._last_collected_energy_timestamp is not None + and prev_address_timestamp is not None + and ( + prev_address_timestamp - self._last_collected_energy_timestamp + ).total_seconds() // 60 > (4 * 60) + 5 # minutes + ): _LOGGER.debug("Energy data collected is outdated, stopping collection") break + + if self._last_collected_energy_timestamp is not None: + prev_address_timestamp = self._last_collected_energy_timestamp + log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 From 982e421a6b57596df11cb4f0a2132d3ca088ea3a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Jun 2025 14:59:33 +0200 Subject: [PATCH 35/74] Fixes, improvements --- plugwise_usb/nodes/circle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 88da02afa..1e26ef96a 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1214,7 +1214,7 @@ async def energy_reset_request(self) -> None: """Send an energy-reset to a Node.""" if self._node_protocols is None: raise NodeError( - "Unable to energy-rest when protocol version is unknown" + "Unable to energy-reset when protocol version is unknown" ) request = CircleClockSetRequest( @@ -1232,7 +1232,7 @@ async def energy_reset_request(self) -> None: f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" ) - _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_bytes) + _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_str) # Follow up by an energy-intervals (re)set request = CircleMeasureIntervalRequest( @@ -1252,9 +1252,9 @@ async def energy_reset_request(self) -> None: # Clear the cached energy_collection if self._cache_enabled: - self._node_cache.update_state(CACHE_ENERGY_COLLECTION, "") + self._set_cache(CACHE_ENERGY_COLLECTION, "") _LOGGER.warning("Energy-collection cache cleared successfully") - await self._node_cache.save_cache() + await self.save_cache() # Clear PulseCollection._logs self._energy_counters.reset_pulse_collection() From 13f202b34a704a6e4cea158f5418f6303933b2ab Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Jun 2025 15:06:48 +0200 Subject: [PATCH 36/74] Make use of the known energy intervals --- plugwise_usb/nodes/circle.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 1e26ef96a..a5ae50747 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -447,16 +447,22 @@ async def get_missing_energy_logs(self) -> None: total_addresses = 11 log_address = self._current_log_address prev_address_timestamp: datetime | None = None + log_interval = self.energy_consumption_interval + factor = 4 + if self.energy_production_interval is not None: + factor = 2 + while total_addresses > 0: await self.energy_log_update(log_address) - # Check if the most recent timestamp of an earlier address is recent (4 * log_interval plus 5 mins margin) + # Check if the most recent timestamp of an earlier address is recent + # (within 2/4 * log_interval plus 5 mins margin) if ( log_address != self._current_log_address and self._last_collected_energy_timestamp is not None and prev_address_timestamp is not None and ( prev_address_timestamp - self._last_collected_energy_timestamp - ).total_seconds() // 60 > (4 * 60) + 5 # minutes + ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes ): _LOGGER.debug("Energy data collected is outdated, stopping collection") break From f112cab0d6efca269dc1f841a8243520bf4c6835 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 08:06:18 +0200 Subject: [PATCH 37/74] Also check if energy data from the current address is recent - likely to be not after an energy-reset --- plugwise_usb/nodes/circle.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index a5ae50747..1f881f549 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -454,8 +454,19 @@ async def get_missing_energy_logs(self) -> None: while total_addresses > 0: await self.energy_log_update(log_address) - # Check if the most recent timestamp of an earlier address is recent + # Check if the most recent timestamp of the current address is recent # (within 2/4 * log_interval plus 5 mins margin) + if ( + log_address == self._current_log_address + and self._last_collected_energy_timestamp is not None + and ( + datetime.now(tz=UTC) - self._last_collected_energy_timestamp + ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes + ): + _LOGGER.debug("Energy data collected is outdated, stopping collection") + break + + # Check if the most recent timestamp of an earlier address is recent if ( log_address != self._current_log_address and self._last_collected_energy_timestamp is not None From 6852bd8935759312cc9c0d1325de696b6b948943 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 08:34:31 +0200 Subject: [PATCH 38/74] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9592432b4..d3a1c5e79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a2" +version = "0.44.4a3" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 46dca9a1a37629fc7a32543a4f554cb9cd41fe62 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 10:15:26 +0200 Subject: [PATCH 39/74] Skip loading node_info from cache when Node is available - the data is already available from a NodeInfoRequest --- plugwise_usb/nodes/node.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index 91c7fb797..c30de7ea3 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -51,6 +51,7 @@ CACHE_NODE_TYPE = "node_type" CACHE_HARDWARE = "hardware" CACHE_NODE_INFO_TIMESTAMP = "node_info_timestamp" +CACHE_RELAY = "relay" class PlugwiseBaseNode(FeaturePublisher, ABC): @@ -456,24 +457,30 @@ async def node_info_update( _LOGGER.debug("No response for node_info_update() for %s", self.mac) await self._available_update_state(False) return self._node_info + await self._available_update_state(True, node_info.timestamp) await self.update_node_details(node_info) return self._node_info async def _node_info_load_from_cache(self) -> bool: """Load node info settings from cache.""" + if self._available: + # Skip loading this data from cache when the Node is available + return True + firmware = self._get_cache_as_datetime(CACHE_FIRMWARE) hardware = self._get_cache(CACHE_HARDWARE) - timestamp = self._get_cache_as_datetime(CACHE_NODE_INFO_TIMESTAMP) node_type: NodeType | None = None if (node_type_str := self._get_cache(CACHE_NODE_TYPE)) is not None: node_type = NodeType(int(node_type_str)) + relay_state = self._get_cache(CACHE_RELAY) node_info = NodeInfoMessage( firmware=firmware, hardware=hardware, + logaddress_pointer=None, node_type=node_type, + relay_state=relay_state, timestamp=timestamp, - relay_state=None, current_logaddress_pointer=None, ) return await self.update_node_details(node_info) From 0b8b60ba63b6ebba2551cf605e7d659cf91362ff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 11:06:49 +0200 Subject: [PATCH 40/74] Improve energy-log-caching debug-logging --- plugwise_usb/nodes/circle.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 1f881f549..36a438c65 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -622,6 +622,7 @@ async def _energy_log_records_save_to_cache(self) -> None: """Save currently collected energy logs to cached file.""" if not self._cache_enabled: return + logs: dict[int, dict[int, PulseLogRecord]] = ( self._energy_counters.get_pulse_logs() ) @@ -635,6 +636,8 @@ async def _energy_log_records_save_to_cache(self) -> None: cached_logs += f"-{log.timestamp.month}-{log.timestamp.day}" cached_logs += f"-{log.timestamp.hour}-{log.timestamp.minute}" cached_logs += f"-{log.timestamp.second}:{log.pulses}" + + _LOGGER.debu("Saving energy logrecords to cache for %s", self._mac_in_str) self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs) async def _energy_log_record_update_state( @@ -659,7 +662,7 @@ async def _energy_log_record_update_state( if (cached_logs := self._get_cache(CACHE_ENERGY_COLLECTION)) is not None: if log_cache_record not in cached_logs: _LOGGER.debug( - "Add logrecord (%s, %s) to log cache of %s", + "Adding logrecord (%s, %s) to cache of %s", str(address), str(slot), self._mac_in_str, @@ -669,10 +672,16 @@ async def _energy_log_record_update_state( ) return True + _LOGGER.debug( + "Energy logrecord already present for %s, ignoring", self._mac_in_str + ) return False _LOGGER.debug( - "No existing energy collection log cached for %s", self._mac_in_str + "Cache is empty, adding new logrecord (%s, %s) for %s", + str(address), + str(slot), + self._mac_in_str ) self._set_cache(CACHE_ENERGY_COLLECTION, log_cache_record) return True From 9247f3cb0f71038594c31a765e03eb0c1becbc69 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 11:42:19 +0200 Subject: [PATCH 41/74] Fixes --- plugwise_usb/nodes/circle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 36a438c65..b06e95460 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -459,6 +459,7 @@ async def get_missing_energy_logs(self) -> None: if ( log_address == self._current_log_address and self._last_collected_energy_timestamp is not None + and log_interval is not None and ( datetime.now(tz=UTC) - self._last_collected_energy_timestamp ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes @@ -470,6 +471,7 @@ async def get_missing_energy_logs(self) -> None: if ( log_address != self._current_log_address and self._last_collected_energy_timestamp is not None + and log_interval is not None and prev_address_timestamp is not None and ( prev_address_timestamp - self._last_collected_energy_timestamp @@ -637,7 +639,7 @@ async def _energy_log_records_save_to_cache(self) -> None: cached_logs += f"-{log.timestamp.hour}-{log.timestamp.minute}" cached_logs += f"-{log.timestamp.second}:{log.pulses}" - _LOGGER.debu("Saving energy logrecords to cache for %s", self._mac_in_str) + _LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str) self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs) async def _energy_log_record_update_state( From a193d7a83577d7080886095c2e97f5ee07c4bb37 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 11:47:14 +0200 Subject: [PATCH 42/74] Sort more update_node_info() arguments --- tests/test_usb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index b94f94f96..0888b72cc 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2158,8 +2158,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign node_info = pw_api.NodeInfoMessage( firmware=dt(2011, 6, 27, 8, 55, 44, tzinfo=UTC), hardware="080007", + logaddress_pointer=None, node_type=None, - timestamp=None, relay_state=None, current_logaddress_pointer=None, ) @@ -2263,8 +2263,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign node_info = pw_api.NodeInfoMessage( firmware=dt(2011, 6, 27, 8, 55, 44, tzinfo=UTC), hardware="080007", + logaddress_pointer=None, node_type=None, - timestamp=None, relay_state=None, current_logaddress_pointer=None, ) @@ -2340,8 +2340,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign node_info = pw_api.NodeInfoMessage( firmware=dt(2011, 6, 27, 9, 4, 10, tzinfo=UTC), hardware="070051", + logaddress_pointer=None, node_type=None, - timestamp=None, relay_state=None, current_logaddress_pointer=None, ) @@ -2358,8 +2358,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign node_info = pw_api.NodeInfoMessage( firmware=dt(2011, 6, 27, 9, 4, 10, tzinfo=UTC), hardware="070051", + logaddress_pointer=None, node_type=None, - timestamp=None, relay_state=None, current_logaddress_pointer=None, ) From 4505460402bb8c66e6adc2dc54a4143fd73958c4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 12:54:07 +0200 Subject: [PATCH 43/74] Add/update cache-related logging --- plugwise_usb/nodes/circle.py | 22 ++++++++++++++++++---- plugwise_usb/nodes/node.py | 3 +++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index b06e95460..0045a3f58 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -269,6 +269,7 @@ async def _calibration_update_state( self._set_cache(CACHE_CALIBRATION_GAIN_B, gain_b) self._set_cache(CACHE_CALIBRATION_NOISE, off_noise) self._set_cache(CACHE_CALIBRATION_TOT, off_tot) + _LOGGER.debug("Saving calibration update to cache for %s", self._mac_in_str) await self.save_cache() return True @@ -558,6 +559,7 @@ async def energy_log_update(self, address: int | None) -> bool: self._energy_counters.update() if energy_record_update: + _LOGGER.debug("Saving energy record update to cache for %s", self._mac_in_str) await self.save_cache() return True @@ -777,6 +779,7 @@ async def _relay_update_state( await self.publish_feature_update_to_subscribers( NodeFeature.RELAY, self._relay_state ) + _LOGGER.debug("Saving relay state update to cache for %s", self._mac_in_str) await self.save_cache() async def _relay_update_lock(self, state: bool) -> None: @@ -796,6 +799,7 @@ async def _relay_update_lock(self, state: bool) -> None: await self.publish_feature_update_to_subscribers( NodeFeature.RELAY_LOCK, self._relay_lock ) + _LOGGER.debug("Saving relay lock state update to cachefor %s", self._mac_in_str) await self.save_cache() async def clock_synchronize(self) -> bool: @@ -1131,6 +1135,7 @@ async def _relay_init_update_state(self, state: bool) -> None: await self.publish_feature_update_to_subscribers( NodeFeature.RELAY_INIT, self._relay_config ) + _LOGGER.debug("Saving relay_init state update to cachefor %s", self._mac_in_str) await self.save_cache() @raise_calibration_missing @@ -1270,7 +1275,7 @@ async def energy_reset_request(self) -> None: NO_PRODUCTION_INTERVAL, ) if (response := await request.send()) is None: - raise NodeError("No response for CircleMeasureIntervalRequest.") + raise NodeError("No response for CircleMeasureIntervalRequest") if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: raise MessageError( @@ -1281,7 +1286,10 @@ async def energy_reset_request(self) -> None: # Clear the cached energy_collection if self._cache_enabled: self._set_cache(CACHE_ENERGY_COLLECTION, "") - _LOGGER.warning("Energy-collection cache cleared successfully") + _LOGGER.warning( + "Energy-collection cache cleared successfully, updating cache for %s", + self._mac_in_str, + ) await self.save_cache() # Clear PulseCollection._logs @@ -1290,6 +1298,12 @@ async def energy_reset_request(self) -> None: # Request a NodeInfo update if await self.node_info_update() is None: - _LOGGER.warning("Node info update failed after energy-reset") + _LOGGER.warning( + "Node info update failed after energy-reset for %s", + self._mac_in_str, + ) else: - _LOGGER.warning("Node info update after energy-reset successful") + _LOGGER.warning( + "Node info update after energy-reset successful for %s", + self._mac_in_str, + ) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index c30de7ea3..dcf40e2a6 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -577,6 +577,8 @@ def _update_node_details_hardware(self, hardware: str | None) -> bool: self._set_cache(CACHE_HARDWARE, hardware) return True + _LOGGER.debug("Saving Node calibration update to cache for %s" self.mac) + async def is_online(self) -> bool: """Check if node is currently online.""" if await self.ping_update() is None: @@ -646,6 +648,7 @@ async def unload(self) -> None: return if self._cache_save_task is not None and not self._cache_save_task.done(): await self._cache_save_task + _LOGGER.debug("Writing cache to disk while unloading for %s" self.mac) await self.save_cache(trigger_only=False, full_write=True) def _get_cache(self, setting: str) -> str | None: From d8b1cc9da1cc4951c4af3707374e9d7f215cea13 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 13:05:15 +0200 Subject: [PATCH 44/74] Fixes --- plugwise_usb/nodes/node.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index dcf40e2a6..bae1bf96a 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -473,8 +473,8 @@ async def _node_info_load_from_cache(self) -> bool: node_type: NodeType | None = None if (node_type_str := self._get_cache(CACHE_NODE_TYPE)) is not None: node_type = NodeType(int(node_type_str)) - relay_state = self._get_cache(CACHE_RELAY) - node_info = NodeInfoMessage( + relay_state = self._get_cache(CACHE_RELAY) == "True" + timestamp = self._get_cache_as_datetime(CACHE_NODE_INFO_TIMESTAMP) firmware=firmware, hardware=hardware, logaddress_pointer=None, @@ -648,7 +648,7 @@ async def unload(self) -> None: return if self._cache_save_task is not None and not self._cache_save_task.done(): await self._cache_save_task - _LOGGER.debug("Writing cache to disk while unloading for %s" self.mac) + _LOGGER.debug("Writing cache to disk while unloading for %s", self.mac) await self.save_cache(trigger_only=False, full_write=True) def _get_cache(self, setting: str) -> str | None: From 6f2f4e96fc997969385e176d902521b992c468b7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 13:45:43 +0200 Subject: [PATCH 45/74] Bump to a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d3a1c5e79..ae6edbcb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a3" +version = "0.44.4a4" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From b9a3c95ee907a08b9a6b6cacbd634ae2314d3bf7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 15:46:31 +0200 Subject: [PATCH 46/74] Don't save calibration data back to cache when loaded before --- plugwise_usb/nodes/circle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 0045a3f58..372f99226 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -236,6 +236,7 @@ async def _calibration_load_from_cache(self) -> bool: cal_gain_b, cal_noise, cal_tot, + load_from_cache=True, ) if result: _LOGGER.debug( @@ -254,6 +255,7 @@ async def _calibration_update_state( gain_b: float | None, off_noise: float | None, off_tot: float | None, + load_from_cache=False, ) -> bool: """Process new energy calibration settings. Returns True if successful.""" if gain_a is None or gain_b is None or off_noise is None or off_tot is None: @@ -263,13 +265,11 @@ async def _calibration_update_state( ) # Forward calibration config to energy collection self._energy_counters.calibration = self._calibration - - if self._cache_enabled: + if self._cache_enabled and not load_from_cache: self._set_cache(CACHE_CALIBRATION_GAIN_A, gain_a) self._set_cache(CACHE_CALIBRATION_GAIN_B, gain_b) self._set_cache(CACHE_CALIBRATION_NOISE, off_noise) self._set_cache(CACHE_CALIBRATION_TOT, off_tot) - _LOGGER.debug("Saving calibration update to cache for %s", self._mac_in_str) await self.save_cache() return True From f1b1b0dc395905531b6aae6ab6a204ff30cf2f0e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 16:28:01 +0200 Subject: [PATCH 47/74] Don't save relay_lock state to back to cache when just loaded from cache --- plugwise_usb/nodes/circle.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 372f99226..177d99af2 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -700,7 +700,7 @@ async def set_relay(self, state: bool) -> bool: if self._relay_lock.state: _LOGGER.debug("Relay switch blocked, relay is locked") - return not state + return self._relay_state.state _LOGGER.debug("Switching relay to %s", state) request = CircleRelaySwitchRequest(self._send, self._mac_in_bytes, state) @@ -753,7 +753,7 @@ async def _relay_load_from_cache(self) -> bool: cached_relay_lock, ) relay_lock = cached_relay_lock == "True" - await self._relay_update_lock(relay_lock) + await self._relay_update_lock(relay_lock, load_from_cache=True) else: # Set to initial state False when not present in cache await self._relay_update_lock(False) @@ -782,7 +782,7 @@ async def _relay_update_state( _LOGGER.debug("Saving relay state update to cache for %s", self._mac_in_str) await self.save_cache() - async def _relay_update_lock(self, state: bool) -> None: + async def _relay_update_lock(self, state: bool, load_from_cache=False) -> None: """Process relay lock update.""" state_update = False if state: @@ -799,8 +799,9 @@ async def _relay_update_lock(self, state: bool) -> None: await self.publish_feature_update_to_subscribers( NodeFeature.RELAY_LOCK, self._relay_lock ) - _LOGGER.debug("Saving relay lock state update to cachefor %s", self._mac_in_str) - await self.save_cache() + if not load_from_cache: + _LOGGER.debug("Saving relay lock state update to cache for %s", self._mac_in_str) + await self.save_cache() async def clock_synchronize(self) -> bool: """Synchronize clock. Returns true if successful.""" From 95c67949d05aee2dc9424a67e5139bcb30fc823a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 16:44:04 +0200 Subject: [PATCH 48/74] Bump to a5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ae6edbcb8..669a4a936 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a4" +version = "0.44.4a5" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From e3385d238c13924afdd92da75e687a5127e5ec7d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 16:57:23 +0200 Subject: [PATCH 49/74] Limit missing logs collection to MAX_LOG_HOURS time period --- plugwise_usb/nodes/helpers/pulses.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 2db5aa56f..8fbb11669 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -890,9 +890,13 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: return None # Collect any missing address in current range + count = 0 address = last_address slot = last_slot - while not (address == first_address and slot == first_slot): + while not ( + (address == first_address and slot == first_slot) + or count > MAX_LOG_HOURS + ): address, slot = calc_log_address(address, slot, -1) if address in missing: continue @@ -902,6 +906,8 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: if self._logs[address][slot].timestamp <= from_timestamp: break + count += 1 + # return missing logs in range first if len(missing) > 0: _LOGGER.debug( From 12d903e2ad6d1a0aab748f886325e97cae67fed1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Jun 2025 17:01:33 +0200 Subject: [PATCH 50/74] Bump to a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 669a4a936..6bd3808a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a5" +version = "0.44.4a6" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 339b6b09e77b30b8127802eee23c04ee030aed7c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Jun 2025 08:28:39 +0200 Subject: [PATCH 51/74] Add more _logs_missing() debugging --- plugwise_usb/nodes/helpers/pulses.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 8fbb11669..4bf1be232 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -892,6 +892,9 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: # Collect any missing address in current range count = 0 address = last_address + _LOGGER.debug( + "_logs_missing | %s | last_address=%s", self._mac, last_address + ) slot = last_slot while not ( (address == first_address and slot == first_slot) @@ -900,7 +903,9 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: address, slot = calc_log_address(address, slot, -1) if address in missing: continue + _LOGGER.debug("address=%s, slot=%s", address, slot) if not self._log_exists(address, slot): + _LOGGER.debug("Address-slot without log: %s, %s", address, slot) missing.append(address) continue if self._logs[address][slot].timestamp <= from_timestamp: From b6aaa9958f3f64e2c01a2189913046d53a5e1c14 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Jun 2025 08:30:11 +0200 Subject: [PATCH 52/74] Bump to a7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6bd3808a8..ac48024e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a6" +version = "0.44.4a7" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 4dc7b3324bb727629fbba9a2cfb04600670530e2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Jun 2025 09:35:44 +0200 Subject: [PATCH 53/74] Remove debug-logging, correct for double production logs --- plugwise_usb/nodes/helpers/pulses.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 4bf1be232..f219de67c 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -889,13 +889,13 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: # Power consumption logging, so we need at least 4 logs. return None - # Collect any missing address in current range - count = 0 + # Collect any missing address in current range, within MAX_LOG_HOURS timeframe address = last_address - _LOGGER.debug( - "_logs_missing | %s | last_address=%s", self._mac, last_address - ) + count = 0 slot = last_slot + if self._log_production: + MAX_LOG_HOURS = 2 * MAX_LOG_HOURS # this requires production_interval == consumption_interval + while not ( (address == first_address and slot == first_slot) or count > MAX_LOG_HOURS @@ -903,11 +903,11 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: address, slot = calc_log_address(address, slot, -1) if address in missing: continue - _LOGGER.debug("address=%s, slot=%s", address, slot) + if not self._log_exists(address, slot): - _LOGGER.debug("Address-slot without log: %s, %s", address, slot) missing.append(address) continue + if self._logs[address][slot].timestamp <= from_timestamp: break From aa35356610bf4a6fa8ca90203b7d22ab05d2956a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Jun 2025 09:42:40 +0200 Subject: [PATCH 54/74] Fix issue, bump to a8 --- plugwise_usb/nodes/helpers/pulses.py | 7 ++++--- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index f219de67c..966ac8f86 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -891,14 +891,15 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: # Collect any missing address in current range, within MAX_LOG_HOURS timeframe address = last_address - count = 0 slot = last_slot + count = 0 + max_count = MAX_LOG_HOURS if self._log_production: - MAX_LOG_HOURS = 2 * MAX_LOG_HOURS # this requires production_interval == consumption_interval + max_count = 2 * max_count # this requires production_interval == consumption_interval while not ( (address == first_address and slot == first_slot) - or count > MAX_LOG_HOURS + or count > max_count ): address, slot = calc_log_address(address, slot, -1) if address in missing: diff --git a/pyproject.toml b/pyproject.toml index ac48024e4..410592723 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a7" +version = "0.44.4a8" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 2b2c0e2bc5cdbbf0cf8505ab80070960538078a0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Jun 2025 09:55:42 +0200 Subject: [PATCH 55/74] Add explanation for max_count-guarding --- plugwise_usb/nodes/helpers/pulses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 966ac8f86..499666cfc 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -890,6 +890,8 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: return None # Collect any missing address in current range, within MAX_LOG_HOURS timeframe + # The max_count-guarding has been added for when an outdated logrecord is present in the cache, + # this will result in the unwanted collection of missing logs outside the MAX_LOG_HOURS timeframe address = last_address slot = last_slot count = 0 From e8b30e3a96579534199b6ccc24b4893257f7c515 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Jun 2025 20:08:35 +0200 Subject: [PATCH 56/74] Start adding coverage for None-data in address 0 --- plugwise_usb/nodes/circle.py | 48 ++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 177d99af2..448af49ce 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -446,9 +446,11 @@ async def get_missing_energy_logs(self) -> None: self._mac_in_str, ) total_addresses = 11 + _LOGGER.debug("_last_collected_energy_timestamp: %s", self._last_collected_energy_timestamp) log_address = self._current_log_address prev_address_timestamp: datetime | None = None log_interval = self.energy_consumption_interval + _LOGGER.debug("log_interval: %s", log_interval) factor = 4 if self.energy_production_interval is not None: factor = 2 @@ -457,29 +459,33 @@ async def get_missing_energy_logs(self) -> None: await self.energy_log_update(log_address) # Check if the most recent timestamp of the current address is recent # (within 2/4 * log_interval plus 5 mins margin) - if ( - log_address == self._current_log_address - and self._last_collected_energy_timestamp is not None - and log_interval is not None - and ( - datetime.now(tz=UTC) - self._last_collected_energy_timestamp - ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes - ): - _LOGGER.debug("Energy data collected is outdated, stopping collection") - break + if log_address == self._current_log_address: + if ( + self._last_collected_energy_timestamp is not None + and log_interval is not None + and ( + datetime.now(tz=UTC) - self._last_collected_energy_timestamp + ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes + ): + _LOGGER.debug("Energy data collected is outdated, stopping collection") + break + + "if pulses=None and timestamp == None:" + " break" # Check if the most recent timestamp of an earlier address is recent - if ( - log_address != self._current_log_address - and self._last_collected_energy_timestamp is not None - and log_interval is not None - and prev_address_timestamp is not None - and ( - prev_address_timestamp - self._last_collected_energy_timestamp - ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes - ): - _LOGGER.debug("Energy data collected is outdated, stopping collection") - break + else: + _LOGGER.debug("prev_address_timestamp: %s", prev_address_timestamp) + if ( + self._last_collected_energy_timestamp is not None + and log_interval is not None + and prev_address_timestamp is not None + and ( + prev_address_timestamp - self._last_collected_energy_timestamp + ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes + ): + _LOGGER.debug("Energy data collected is outdated, stopping collection") + break if self._last_collected_energy_timestamp is not None: prev_address_timestamp = self._last_collected_energy_timestamp From dad5576e1456f427c05b63d66ba4506e315ee218 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 09:17:40 +0200 Subject: [PATCH 57/74] Handle no data present in address 0 --- plugwise_usb/nodes/circle.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 448af49ce..22efd7e99 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -446,7 +446,6 @@ async def get_missing_energy_logs(self) -> None: self._mac_in_str, ) total_addresses = 11 - _LOGGER.debug("_last_collected_energy_timestamp: %s", self._last_collected_energy_timestamp) log_address = self._current_log_address prev_address_timestamp: datetime | None = None log_interval = self.energy_consumption_interval @@ -458,20 +457,25 @@ async def get_missing_energy_logs(self) -> None: while total_addresses > 0: await self.energy_log_update(log_address) # Check if the most recent timestamp of the current address is recent - # (within 2/4 * log_interval plus 5 mins margin) + # (within 2/4 * log_interval plus 5 mins margin), or None if log_address == self._current_log_address: - if ( - self._last_collected_energy_timestamp is not None - and log_interval is not None + if self._last_collected_energy_timestamp is None: + # Handle case with no data in slot 0 + _LOGGER.debug( + "Energy data collected from the current log address is None, stopping collection" + ) + break + + elif ( + log_interval is not None and ( datetime.now(tz=UTC) - self._last_collected_energy_timestamp ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes ): - _LOGGER.debug("Energy data collected is outdated, stopping collection") + _LOGGER.debug( + "Energy data collected from the current log address is outdated, stopping collection" + ) break - - "if pulses=None and timestamp == None:" - " break" # Check if the most recent timestamp of an earlier address is recent else: @@ -484,7 +488,7 @@ async def get_missing_energy_logs(self) -> None: prev_address_timestamp - self._last_collected_energy_timestamp ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes ): - _LOGGER.debug("Energy data collected is outdated, stopping collection") + _LOGGER.debug("Collected energy data is outdated, stopping collection") break if self._last_collected_energy_timestamp is not None: From 495b2c4754245343bec139d5ea6fa065ad39e2c3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 09:43:02 +0200 Subject: [PATCH 58/74] Bump to a9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 410592723..b32e53452 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a8" +version = "0.44.4a9" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 911d89c5767d2a2c03c4975aeccd0d8fee333060 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 11:40:13 +0200 Subject: [PATCH 59/74] Change logic: collect energy_logs for address 0 first, then check if earlier address contains outdated data --- plugwise_usb/nodes/circle.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 22efd7e99..29771dd42 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -448,16 +448,8 @@ async def get_missing_energy_logs(self) -> None: total_addresses = 11 log_address = self._current_log_address prev_address_timestamp: datetime | None = None - log_interval = self.energy_consumption_interval - _LOGGER.debug("log_interval: %s", log_interval) - factor = 4 - if self.energy_production_interval is not None: - factor = 2 - while total_addresses > 0: await self.energy_log_update(log_address) - # Check if the most recent timestamp of the current address is recent - # (within 2/4 * log_interval plus 5 mins margin), or None if log_address == self._current_log_address: if self._last_collected_energy_timestamp is None: # Handle case with no data in slot 0 @@ -466,19 +458,16 @@ async def get_missing_energy_logs(self) -> None: ) break - elif ( - log_interval is not None - and ( - datetime.now(tz=UTC) - self._last_collected_energy_timestamp - ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes - ): - _LOGGER.debug( - "Energy data collected from the current log address is outdated, stopping collection" - ) - break - # Check if the most recent timestamp of an earlier address is recent + # (within 2/4 * log_interval plus 5 mins margin) else: + log_interval = self.energy_consumption_interval + _LOGGER.debug("log_interval: %s", log_interval) + _LOGGER.debug("energy_production_interval: %s", self.energy_production_interval) + factor = 4 + if self.energy_production_interval is not None: + factor = 2 + _LOGGER.debug("prev_address_timestamp: %s", prev_address_timestamp) if ( self._last_collected_energy_timestamp is not None From 03e614c3586f9fab307287e2969ae69889084b3f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 11:40:51 +0200 Subject: [PATCH 60/74] Updates by ruff --- plugwise_usb/nodes/circle.py | 40 +++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 29771dd42..191cb0c1e 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -463,7 +463,10 @@ async def get_missing_energy_logs(self) -> None: else: log_interval = self.energy_consumption_interval _LOGGER.debug("log_interval: %s", log_interval) - _LOGGER.debug("energy_production_interval: %s", self.energy_production_interval) + _LOGGER.debug( + "energy_production_interval: %s", + self.energy_production_interval, + ) factor = 4 if self.energy_production_interval is not None: factor = 2 @@ -474,10 +477,15 @@ async def get_missing_energy_logs(self) -> None: and log_interval is not None and prev_address_timestamp is not None and ( - prev_address_timestamp - self._last_collected_energy_timestamp - ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes + prev_address_timestamp + - self._last_collected_energy_timestamp + ).total_seconds() + // 60 + > (factor * log_interval) + 5 # minutes ): - _LOGGER.debug("Collected energy data is outdated, stopping collection") + _LOGGER.debug( + "Collected energy data is outdated, stopping collection" + ) break if self._last_collected_energy_timestamp is not None: @@ -551,14 +559,16 @@ async def energy_log_update(self, address: int | None) -> bool: ): energy_record_update = True if not last_energy_timestamp_collected: - self._last_collected_energy_timestamp = ( - log_timestamp.replace(tzinfo=UTC) + self._last_collected_energy_timestamp = log_timestamp.replace( + tzinfo=UTC ) last_energy_timestamp_collected = True self._energy_counters.update() if energy_record_update: - _LOGGER.debug("Saving energy record update to cache for %s", self._mac_in_str) + _LOGGER.debug( + "Saving energy record update to cache for %s", self._mac_in_str + ) await self.save_cache() return True @@ -639,7 +649,7 @@ async def _energy_log_records_save_to_cache(self) -> None: cached_logs += f"-{log.timestamp.month}-{log.timestamp.day}" cached_logs += f"-{log.timestamp.hour}-{log.timestamp.minute}" cached_logs += f"-{log.timestamp.second}:{log.pulses}" - + _LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str) self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs) @@ -684,7 +694,7 @@ async def _energy_log_record_update_state( "Cache is empty, adding new logrecord (%s, %s) for %s", str(address), str(slot), - self._mac_in_str + self._mac_in_str, ) self._set_cache(CACHE_ENERGY_COLLECTION, log_cache_record) return True @@ -799,7 +809,9 @@ async def _relay_update_lock(self, state: bool, load_from_cache=False) -> None: NodeFeature.RELAY_LOCK, self._relay_lock ) if not load_from_cache: - _LOGGER.debug("Saving relay lock state update to cache for %s", self._mac_in_str) + _LOGGER.debug( + "Saving relay lock state update to cache for %s", self._mac_in_str + ) await self.save_cache() async def clock_synchronize(self) -> bool: @@ -1135,7 +1147,9 @@ async def _relay_init_update_state(self, state: bool) -> None: await self.publish_feature_update_to_subscribers( NodeFeature.RELAY_INIT, self._relay_config ) - _LOGGER.debug("Saving relay_init state update to cachefor %s", self._mac_in_str) + _LOGGER.debug( + "Saving relay_init state update to cachefor %s", self._mac_in_str + ) await self.save_cache() @raise_calibration_missing @@ -1246,9 +1260,7 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any async def energy_reset_request(self) -> None: """Send an energy-reset to a Node.""" if self._node_protocols is None: - raise NodeError( - "Unable to energy-reset when protocol version is unknown" - ) + raise NodeError("Unable to energy-reset when protocol version is unknown") request = CircleClockSetRequest( self._send, From cb5324a815e6c6a73279fe72c8b2b0ff4f78567a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 11:49:54 +0200 Subject: [PATCH 61/74] Bump to a10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b32e53452..645981af3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a9" +version = "0.44.4a10" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 3ba634b00d7e56ce61eddbf5c4fdeb5b7d93eec1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 13:37:08 +0200 Subject: [PATCH 62/74] Improve energy_log_update() output logic --- plugwise_usb/nodes/circle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 191cb0c1e..33382ce46 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -521,6 +521,7 @@ async def energy_log_update(self, address: int | None) -> bool: """Request energy log statistics from node. Returns true if successful.""" if address is None: return False + _LOGGER.debug( "Request of energy log at address %s for node %s", str(address), @@ -570,8 +571,9 @@ async def energy_log_update(self, address: int | None) -> bool: "Saving energy record update to cache for %s", self._mac_in_str ) await self.save_cache() + return True - return True + return False async def _energy_log_records_load_from_cache(self) -> bool: """Load energy_log_record from cache.""" From 47a964e3ce99f526f25cbb63424514e44aa0f333 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 13:59:25 +0200 Subject: [PATCH 63/74] Make use of improved energy_log_update() output logic --- plugwise_usb/nodes/circle.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 33382ce46..f75abb400 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -449,18 +449,17 @@ async def get_missing_energy_logs(self) -> None: log_address = self._current_log_address prev_address_timestamp: datetime | None = None while total_addresses > 0: - await self.energy_log_update(log_address) - if log_address == self._current_log_address: - if self._last_collected_energy_timestamp is None: - # Handle case with no data in slot 0 - _LOGGER.debug( - "Energy data collected from the current log address is None, stopping collection" - ) - break + if not await self.energy_log_update(log_address): + # Handle case with None-data in all address slots + _LOGGER.debug( + "Energy None-data collected from log address %s, stopping collection", + log_address, + ) + break # Check if the most recent timestamp of an earlier address is recent # (within 2/4 * log_interval plus 5 mins margin) - else: + if log_address != self._current_log_address: log_interval = self.energy_consumption_interval _LOGGER.debug("log_interval: %s", log_interval) _LOGGER.debug( From 976f53c2bce550e29cdef86dbfba68d4686dc7a2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 14:13:26 +0200 Subject: [PATCH 64/74] Further improvements, add comment, debug-logging --- plugwise_usb/nodes/circle.py | 43 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index f75abb400..8c04566de 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -442,7 +442,7 @@ async def get_missing_energy_logs(self) -> None: if (missing_addresses := self._energy_counters.log_addresses_missing) is None: _LOGGER.debug( - "Start with initial energy requests for the last 10 log addresses for node %s.", + "Start collecting initial energy logs from the last 10 log addresses for node %s.", self._mac_in_str, ) total_addresses = 11 @@ -459,17 +459,33 @@ async def get_missing_energy_logs(self) -> None: # Check if the most recent timestamp of an earlier address is recent # (within 2/4 * log_interval plus 5 mins margin) - if log_address != self._current_log_address: - log_interval = self.energy_consumption_interval - _LOGGER.debug("log_interval: %s", log_interval) - _LOGGER.debug( - "energy_production_interval: %s", - self.energy_production_interval, - ) - factor = 4 - if self.energy_production_interval is not None: - factor = 2 + log_interval = self.energy_consumption_interval + _LOGGER.debug("log_interval: %s", log_interval) + _LOGGER.debug( + "last_collected_energy_timestamp: %s", + self._last_collected_energy_timestamp, + ) + _LOGGER.debug( + "energy_production_interval: %s", + self.energy_production_interval, + ) + factor = 4 + if self.energy_production_interval is not None: + factor = 2 + if log_address == self._current_log_address: + if ( + self._last_collected_energy_timestamp is not None + and log_interval is not None + and ( + datetime.now(tz=UTC) - self._last_collected_energy_timestamp + ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes + ): + _LOGGER.debug( + "Energy data collected from the current log address is outdated, stopping collection" + ) + break + else: _LOGGER.debug("prev_address_timestamp: %s", prev_address_timestamp) if ( self._last_collected_energy_timestamp is not None @@ -559,9 +575,14 @@ async def energy_log_update(self, address: int | None) -> bool: ): energy_record_update = True if not last_energy_timestamp_collected: + # Collect the timestamp of the most recent response self._last_collected_energy_timestamp = log_timestamp.replace( tzinfo=UTC ) + _LOGGER.debug( + "Setting last_collected_energy_timestamp to %s", + self._last_collected_energy_timestamp, + ) last_energy_timestamp_collected = True self._energy_counters.update() From b5edc3c25f1116aff8e97a316fc4202b6e81b8a9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 14:36:08 +0200 Subject: [PATCH 65/74] Bump to a11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 645981af3..c2ff44360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a10" +version = "0.44.4a11" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From ac0b25f2b3b5007bddb0be7411dba477ad99c1af Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 15:53:17 +0200 Subject: [PATCH 66/74] Reduce complexity, improve --- plugwise_usb/nodes/circle.py | 124 +++++++++++++++++------------------ 1 file changed, 60 insertions(+), 64 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 8c04566de..1ad32b27a 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -255,7 +255,7 @@ async def _calibration_update_state( gain_b: float | None, off_noise: float | None, off_tot: float | None, - load_from_cache=False, + load_from_cache: bool = False, ) -> bool: """Process new energy calibration settings. Returns True if successful.""" if gain_a is None or gain_b is None or off_noise is None or off_tot is None: @@ -434,84 +434,78 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return None - async def get_missing_energy_logs(self) -> None: - """Task to retrieve missing energy logs.""" - self._energy_counters.update() + async def _get_initial_energy_logs(self) -> None: + """Collect initial energy logs from the last 10 log addresses.""" if self._current_log_address is None: - return None - - if (missing_addresses := self._energy_counters.log_addresses_missing) is None: - _LOGGER.debug( - "Start collecting initial energy logs from the last 10 log addresses for node %s.", - self._mac_in_str, - ) - total_addresses = 11 - log_address = self._current_log_address - prev_address_timestamp: datetime | None = None - while total_addresses > 0: - if not await self.energy_log_update(log_address): - # Handle case with None-data in all address slots - _LOGGER.debug( - "Energy None-data collected from log address %s, stopping collection", - log_address, - ) - break + return - # Check if the most recent timestamp of an earlier address is recent - # (within 2/4 * log_interval plus 5 mins margin) - log_interval = self.energy_consumption_interval - _LOGGER.debug("log_interval: %s", log_interval) - _LOGGER.debug( - "last_collected_energy_timestamp: %s", - self._last_collected_energy_timestamp, - ) + _LOGGER.debug( + "Start collecting initial energy logs from the last 10 log addresses for node %s.", + self._mac_in_str, + ) + total_addresses = 11 + log_address = self._current_log_address + prev_address_timestamp: datetime | None = None + while total_addresses > 0: + if not await self.energy_log_update(log_address): + # Handle case with None-data in all address slots _LOGGER.debug( - "energy_production_interval: %s", - self.energy_production_interval, + "Energy None-data collected from log address %s, stopping collection", + log_address, ) - factor = 4 - if self.energy_production_interval is not None: - factor = 2 + break + + # Check if the most recent timestamp of an earlier address is recent + # (within 2/4 * log_interval plus 5 mins margin) + log_interval = self.energy_consumption_interval + factor = 2 if self.energy_production_interval is not None else 4 + if log_interval is not None: + max_gap_minutes = (factor * log_interval) + 5 if log_address == self._current_log_address: if ( self._last_collected_energy_timestamp is not None - and log_interval is not None and ( datetime.now(tz=UTC) - self._last_collected_energy_timestamp - ).total_seconds() // 60 > (factor * log_interval) + 5 # minutes - ): - _LOGGER.debug( - "Energy data collected from the current log address is outdated, stopping collection" - ) - break - else: - _LOGGER.debug("prev_address_timestamp: %s", prev_address_timestamp) - if ( - self._last_collected_energy_timestamp is not None - and log_interval is not None - and prev_address_timestamp is not None - and ( - prev_address_timestamp - - self._last_collected_energy_timestamp ).total_seconds() // 60 - > (factor * log_interval) + 5 # minutes + > max_gap_minutes ): _LOGGER.debug( - "Collected energy data is outdated, stopping collection" + "Energy data collected from the current log address is outdated, stopping collection" ) break + elif ( + prev_address_timestamp is not None + and self._last_collected_energy_timestamp is not None + and ( + prev_address_timestamp - self._last_collected_energy_timestamp + ).total_seconds() + // 60 + > max_gap_minutes + ): + _LOGGER.debug( + "Collected energy data is outdated, stopping collection" + ) + break - if self._last_collected_energy_timestamp is not None: - prev_address_timestamp = self._last_collected_energy_timestamp + if self._last_collected_energy_timestamp is not None: + prev_address_timestamp = self._last_collected_energy_timestamp - log_address, _ = calc_log_address(log_address, 1, -4) - total_addresses -= 1 + log_address, _ = calc_log_address(log_address, 1, -4) + total_addresses -= 1 - if self._cache_enabled: - await self._energy_log_records_save_to_cache() + 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() + if self._current_log_address is None: + return + if (missing_addresses := self._energy_counters.log_addresses_missing) is None: + await self._get_initial_energy_logs() return _LOGGER.debug("Task created to get missing logs of %s", self._mac_in_str) @@ -813,7 +807,9 @@ async def _relay_update_state( _LOGGER.debug("Saving relay state update to cache for %s", self._mac_in_str) await self.save_cache() - async def _relay_update_lock(self, state: bool, load_from_cache=False) -> None: + async def _relay_update_lock( + self, state: bool, load_from_cache: bool = False + ) -> None: """Process relay lock update.""" state_update = False if state: @@ -1302,18 +1298,18 @@ async def energy_reset_request(self) -> None: _LOGGER.warning("Energy reset for Node %s successful", self._mac_in_str) # Follow up by an energy-intervals (re)set - request = CircleMeasureIntervalRequest( + interval_request = CircleMeasureIntervalRequest( self._send, self._mac_in_bytes, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL, ) - if (response := await request.send()) is None: + if (interval_response := await interval_request.send()) is None: raise NodeError("No response for CircleMeasureIntervalRequest") - if response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: + if interval_response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: raise MessageError( - f"Unknown NodeResponseType '{response.response_type.name}' received" + f"Unknown NodeResponseType '{interval_response.response_type.name}' received" ) _LOGGER.warning("Resetting energy intervals to default (= consumption only)") From 13271797ae6f373872e5f91bc5211218134c9376 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 16:38:33 +0200 Subject: [PATCH 67/74] Fixes after rebase --- plugwise_usb/nodes/node.py | 7 +++---- tests/test_usb.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index bae1bf96a..1c7cbb4f1 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -475,13 +475,13 @@ async def _node_info_load_from_cache(self) -> bool: node_type = NodeType(int(node_type_str)) relay_state = self._get_cache(CACHE_RELAY) == "True" timestamp = self._get_cache_as_datetime(CACHE_NODE_INFO_TIMESTAMP) + node_info = NodeInfoMessage( + current_logaddress_pointer=None, firmware=firmware, hardware=hardware, - logaddress_pointer=None, node_type=node_type, relay_state=relay_state, timestamp=timestamp, - current_logaddress_pointer=None, ) return await self.update_node_details(node_info) @@ -517,6 +517,7 @@ async def update_node_details( complete &= self._update_node_details_hardware(node_info.hardware) complete &= self._update_node_details_timestamp(node_info.timestamp) + _LOGGER.debug("Saving Node calibration update to cache for %s" self.mac) await self.save_cache() if node_info.timestamp is not None and node_info.timestamp > datetime.now( tz=UTC @@ -577,8 +578,6 @@ def _update_node_details_hardware(self, hardware: str | None) -> bool: self._set_cache(CACHE_HARDWARE, hardware) return True - _LOGGER.debug("Saving Node calibration update to cache for %s" self.mac) - async def is_online(self) -> bool: """Check if node is currently online.""" if await self.ping_update() is None: diff --git a/tests/test_usb.py b/tests/test_usb.py index 0888b72cc..214699e14 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2156,12 +2156,12 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) assert not test_scan.cache_enabled node_info = pw_api.NodeInfoMessage( + current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 8, 55, 44, tzinfo=UTC), hardware="080007", logaddress_pointer=None, node_type=None, relay_state=None, - current_logaddress_pointer=None, ) await test_scan.update_node_details(node_info) assert await test_scan.load() @@ -2261,12 +2261,12 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign "1298347650AFBECD", 1, mock_stick_controller, load_callback ) node_info = pw_api.NodeInfoMessage( + current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 8, 55, 44, tzinfo=UTC), hardware="080007", logaddress_pointer=None, node_type=None, relay_state=None, - current_logaddress_pointer=None, ) await test_scan.update_node_details(node_info) test_scan.cache_enabled = True @@ -2338,12 +2338,12 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) ) node_info = pw_api.NodeInfoMessage( + current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 9, 4, 10, tzinfo=UTC), hardware="070051", logaddress_pointer=None, node_type=None, relay_state=None, - current_logaddress_pointer=None, ) await test_switch.update_node_details(node_info) assert await test_switch.load() @@ -2356,12 +2356,12 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign "1298347650AFBECD", 1, mock_stick_controller, load_callback ) node_info = pw_api.NodeInfoMessage( + current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 9, 4, 10, tzinfo=UTC), hardware="070051", logaddress_pointer=None, node_type=None, relay_state=None, - current_logaddress_pointer=None, ) await test_switch.update_node_details(node_info) test_switch.cache_enabled = True From 1e72e6122a43eea7133482ce3e475a604a47752e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 16:47:55 +0200 Subject: [PATCH 68/74] More fixes --- plugwise_usb/nodes/node.py | 2 +- tests/test_usb.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index 1c7cbb4f1..a8096c829 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -517,7 +517,7 @@ async def update_node_details( complete &= self._update_node_details_hardware(node_info.hardware) complete &= self._update_node_details_timestamp(node_info.timestamp) - _LOGGER.debug("Saving Node calibration update to cache for %s" self.mac) + _LOGGER.debug("Saving Node calibration update to cache for %s", self.mac) await self.save_cache() if node_info.timestamp is not None and node_info.timestamp > datetime.now( tz=UTC diff --git a/tests/test_usb.py b/tests/test_usb.py index 214699e14..e861175cf 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2159,7 +2159,6 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 8, 55, 44, tzinfo=UTC), hardware="080007", - logaddress_pointer=None, node_type=None, relay_state=None, ) @@ -2264,7 +2263,6 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 8, 55, 44, tzinfo=UTC), hardware="080007", - logaddress_pointer=None, node_type=None, relay_state=None, ) @@ -2341,7 +2339,6 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 9, 4, 10, tzinfo=UTC), hardware="070051", - logaddress_pointer=None, node_type=None, relay_state=None, ) @@ -2359,7 +2356,6 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign current_logaddress_pointer=None, firmware=dt(2011, 6, 27, 9, 4, 10, tzinfo=UTC), hardware="070051", - logaddress_pointer=None, node_type=None, relay_state=None, ) From 6e9ce3380c06fcc20c52e6726d85b39c2c046dbf Mon Sep 17 00:00:00 2001 From: autoruff Date: Sat, 21 Jun 2025 14:49:29 +0000 Subject: [PATCH 69/74] fixup: clear-cache Python code reformatted using Ruff --- plugwise_usb/nodes/circle.py | 5 ++++- plugwise_usb/nodes/helpers/counter.py | 2 +- plugwise_usb/nodes/helpers/pulses.py | 9 +++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 1ad32b27a..4e9976137 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1307,7 +1307,10 @@ async def energy_reset_request(self) -> None: if (interval_response := await interval_request.send()) is None: raise NodeError("No response for CircleMeasureIntervalRequest") - if interval_response.response_type != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED: + if ( + interval_response.response_type + != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED + ): raise MessageError( f"Unknown NodeResponseType '{interval_response.response_type.name}' received" ) diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index 14bee2ee1..2caf6828d 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -104,7 +104,7 @@ def add_pulse_stats( self.update() def reset_pulse_collection(self) -> None: - """ Reset the related pulse collection.""" + """Reset the related pulse collection.""" self._pulse_collection.reset() @property diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 499666cfc..442967576 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -533,7 +533,7 @@ def _check_prev_production( """Check the previous slot for production pulses.""" if self._logs is None: return - + prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): prev_timestamp = self._logs[prev_address][prev_slot].timestamp @@ -897,11 +897,12 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: count = 0 max_count = MAX_LOG_HOURS if self._log_production: - max_count = 2 * max_count # this requires production_interval == consumption_interval + max_count = ( + 2 * max_count + ) # this requires production_interval == consumption_interval while not ( - (address == first_address and slot == first_slot) - or count > max_count + (address == first_address and slot == first_slot) or count > max_count ): address, slot = calc_log_address(address, slot, -1) if address in missing: From 98a8ec8e5cd0c3180bcf92acd99461bb5b710410 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 16:54:15 +0200 Subject: [PATCH 70/74] Clean up --- plugwise_usb/network/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 05363dafc..94b61111f 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -14,8 +14,6 @@ from ..connection import StickController from ..constants import ENERGY_NODE_TYPES, UTF8 from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout -from ..nodes.helpers.cache import NodeCache -from ..nodes.helpers.pulses import PulseCollection from ..helpers.util import validate_mac from ..messages.requests import ( CircleClockSetRequest, @@ -35,7 +33,6 @@ from ..nodes import get_plugwise_node from .registry import StickNetworkRegister -CACHE_ENERGY_COLLECTION = "energy_collection" _LOGGER = logging.getLogger(__name__) # endregion From 98785ab0700a0294ad0be652ecbbcbaea3e3c1b5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 17:00:19 +0200 Subject: [PATCH 71/74] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56938429..02d1f74c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## Ongoing +## v0.44.4 +- PR [#255](https://github.com/plugwise/python-plugwise-usb/pull/255): Improve enery-logs reset process, enable use of a button in Plugwise_usb beta - PR [#261](https://github.com/plugwise/python-plugwise-usb/pull/261): Sense: bugfix parsing of humidity value as an unsigned int - PR #263 Maintenance chores and re-instatement of ruff, deprecate pre-commit cloud runs (just leveraging renovate) - PR #264 Maintenance chores Rework Github Actions workflow From c5e18b602245bb72cc53dc6d147ce649a5753aa8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 17:09:19 +0200 Subject: [PATCH 72/74] Set to v0.44.4 release-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c2ff44360..29155a6f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.4a11" +version = "0.44.4" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From cdffa5ea290ae1ce6ec01011821b72049a085b82 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 17:14:43 +0200 Subject: [PATCH 73/74] Ruff fixes/block --- plugwise_usb/network/__init__.py | 8 ++------ plugwise_usb/nodes/helpers/pulses.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 94b61111f..e9262803d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -6,7 +6,7 @@ from asyncio import Task, create_task, gather, sleep from collections.abc import Callable, Coroutine -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta import logging from typing import Any @@ -15,11 +15,7 @@ from ..constants import ENERGY_NODE_TYPES, UTF8 from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac -from ..messages.requests import ( - CircleClockSetRequest, - CircleMeasureIntervalRequest, - NodePingRequest, -) +from ..messages.requests import CircleMeasureIntervalRequest, NodePingRequest from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, NODE_JOIN_ID, diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 442967576..338ed70d9 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -834,7 +834,7 @@ def _first_log_reference( ) return (self._first_log_production_address, self._first_log_production_slot) - def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: PLR0911 PLR0912 + def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # noqa: PLR0911 PLR0912 PLR0915 """Calculate list of missing log addresses.""" if self._logs is None: self._log_addresses_missing = None From a7753c16fa6cbb9c5a2d4c9d9758df90949e9299 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 21 Jun 2025 20:26:37 +0200 Subject: [PATCH 74/74] Add .python-version to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8dad5e02e..6cf4137e2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__ .idea tests/__pycache__ .coverage +.python-version .vscode venv .venv