From 2975a6fe8a79880577b835840e970ed86a83e708 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 08:26:12 +0200 Subject: [PATCH 01/14] Add extra debug logging in circle.py --- 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 682d1193f..3bb0c50d0 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -294,6 +294,7 @@ async def power_update(self) -> PowerStatistics | None: Return power usage or None if retrieval failed """ + _LOGGER.debug("Requesting a PowerStatistics update for %s", self._mac_in_str) # Debounce power if self.skip_update(self._power, MINIMAL_POWER_UPDATE): return self._power @@ -349,6 +350,7 @@ def _log_no_energy_stats_update(self) -> None: @raise_calibration_missing async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR0912 """Return updated energy usage statistics.""" + _LOGGER.debug("Requesting an EnergyStatistics update for %s", self._mac_in_str) if self._current_log_address is None: _LOGGER.debug( "Unable to update energy logs for node %s because the current log address is unknown.", @@ -369,6 +371,9 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 self._current_log_address ) + _LOGGER.debug( + "Rollover status for %s: %s", self._mac_in_str, self._energy_counters.log_rollover + ) if self._energy_counters.log_rollover: # Try updating node_info to collect the updated energy log address if await self.node_info_update() is None: From b4ca5da547f69ed2fd913885341080963794f4e6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 10:51:54 +0200 Subject: [PATCH 02/14] Disable get_state for NodeFeature.ENERGY should be triggered by getting NodeFeature POWER --- plugwise_usb/nodes/circle.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 3bb0c50d0..eed9ec386 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1237,13 +1237,14 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any ) match feature: - case NodeFeature.ENERGY: - states[feature] = await self.energy_update() - _LOGGER.debug( - "async_get_state %s - energy: %s", - self._mac_in_str, - states[feature], - ) + # Disable, getting NodeFeature.POWER will trigger an energy_update + # case NodeFeature.ENERGY: + # states[feature] = await self.energy_update() + # _LOGGER.debug( + # "async_get_state %s - energy: %s", + # self._mac_in_str, + # states[feature], + # ) case NodeFeature.RELAY: states[feature] = self._relay_state _LOGGER.debug( From 4367cf1a0f9140b5c9611a8e7be62ba704cdb4df Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 14:09:50 +0200 Subject: [PATCH 03/14] Revert "Disable get_state for NodeFeature.ENERGY" This reverts commit b0730a35c663a2e8360f1f8e46a8cd6de9369aaa. --- plugwise_usb/nodes/circle.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index eed9ec386..3bb0c50d0 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1237,14 +1237,13 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any ) match feature: - # Disable, getting NodeFeature.POWER will trigger an energy_update - # case NodeFeature.ENERGY: - # states[feature] = await self.energy_update() - # _LOGGER.debug( - # "async_get_state %s - energy: %s", - # self._mac_in_str, - # states[feature], - # ) + case NodeFeature.ENERGY: + states[feature] = await self.energy_update() + _LOGGER.debug( + "async_get_state %s - energy: %s", + self._mac_in_str, + states[feature], + ) case NodeFeature.RELAY: states[feature] = self._relay_state _LOGGER.debug( From 5f0fdc53ef24e2645510a45abaad9aea5066aba5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 15:33:15 +0200 Subject: [PATCH 04/14] Don't call power_update() twice it is called as part of energy_update() --- plugwise_usb/nodes/circle.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 3bb0c50d0..d56e0585a 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1256,7 +1256,8 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any case NodeFeature.RELAY_INIT: states[feature] = self._relay_config case NodeFeature.POWER: - states[feature] = await self.power_update() + # power_update() is called as part of energy_update() + states[feature] = self._power _LOGGER.debug( "async_get_state %s - power: %s", self._mac_in_str, From d6c116d7661f8ab14a47b8a1d1b99d79f8ab8b39 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 15:45:34 +0200 Subject: [PATCH 05/14] Make sure to call power_update() when there are 2 or more missing_logs required because of the removal of the power_update() call in get_state() --- plugwise_usb/nodes/circle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index d56e0585a..dbdeb6226 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -429,6 +429,10 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return self._energy_counters.energy_statistics + # Perform a power_update() when there are more missing logs + # Required because calling power_update() in get_state() has been removed + await self.power_update() + # Create task to request remaining missing logs if ( self._retrieve_energy_logs_task is None From 5232c87e12222ac442254f46809b8376bcb3c5b1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 16:09:37 +0200 Subject: [PATCH 06/14] Revert "Make sure to call power_update() when there are 2 or more missing_logs" This reverts commit d3d4c87cf8d773c21ad0150430c5775b0f89d308. --- plugwise_usb/nodes/circle.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index dbdeb6226..d56e0585a 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -429,10 +429,6 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) return self._energy_counters.energy_statistics - # Perform a power_update() when there are more missing logs - # Required because calling power_update() in get_state() has been removed - await self.power_update() - # Create task to request remaining missing logs if ( self._retrieve_energy_logs_task is None From 03f570ea35f7b49757e52a00f6796ae833962cef Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 16:09:45 +0200 Subject: [PATCH 07/14] Revert "Don't call power_update() twice" This reverts commit c7fb2b0d6c2eb423b6b62edf64b0dba56d08c57f. --- plugwise_usb/nodes/circle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index d56e0585a..3bb0c50d0 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1256,8 +1256,7 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any case NodeFeature.RELAY_INIT: states[feature] = self._relay_config case NodeFeature.POWER: - # power_update() is called as part of energy_update() - states[feature] = self._power + states[feature] = await self.power_update() _LOGGER.debug( "async_get_state %s - power: %s", self._mac_in_str, From 5f25d708afc3966139ad24ef7604d4ce721ebd7a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 18:04:01 +0200 Subject: [PATCH 08/14] Ruff fix --- 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 3bb0c50d0..3c81c9c12 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -372,7 +372,9 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 ) _LOGGER.debug( - "Rollover status for %s: %s", self._mac_in_str, self._energy_counters.log_rollover + "Rollover status for %s: %s", + self._mac_in_str, + self._energy_counters.log_rollover, ) if self._energy_counters.log_rollover: # Try updating node_info to collect the updated energy log address From f76918831ec8e39db813209edc0ef4dd343826a3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 22 Jul 2025 20:28:16 +0200 Subject: [PATCH 09/14] Execute power_update() before energy_update() to allow for in-time detection of pulses rollover events --- plugwise_usb/nodes/circle.py | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 3c81c9c12..00800aa15 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1238,6 +1238,11 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any f"Update of feature '{feature}' is not supported for {self.name}" ) + features, states = await self._check_for_energy_and_power_features( + features, states + ) + + for feature in features: match feature: case NodeFeature.ENERGY: states[feature] = await self.energy_update() @@ -1273,6 +1278,38 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any return states + async def _check_for_energy_and_power_features( + self, features: tuple[NodeFeature], states: dict[NodeFeature, Any] + ) -> tuple[tuple[NodeFeature], dict[NodeFeature, Any]]: + """Check for presence of both NodeFeature.ENERGY and NodeFeature.POWER. + + If both are present, execute the related functions in a specific order + to assure a proper response to the hourly pulses-rollovers. + """ + if NodeFeature.ENERGY in features and NodeFeature.POWER in features: + states[NodeFeature.POWER] = await self.power_update() + _LOGGER.debug( + "async_get_state %s - power: %s", + self._mac_in_str, + states[NodeFeature.POWER], + ) + states[NodeFeature.ENERGY] = await self.energy_update() + _LOGGER.debug( + "async_get_state %s - energy: %s", + self._mac_in_str, + states[NodeFeature.ENERGY], + ) + + remaining_features: tuple[NodeFeature, ...] = () + for feature in features: + if feature in [NodeFeature.ENERGY, NodeFeature.POWER]: + continue + remaining_features += (feature,) + + return remaining_features, states + + return features, states + async def energy_reset_request(self) -> None: """Send an energy-reset to a Node.""" if self._node_protocols is None: From b6104e7aec6838d7859030758c95a86d807eef45 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 23 Jul 2025 08:44:07 +0200 Subject: [PATCH 10/14] CRAI suggestion --- plugwise_usb/nodes/circle.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 00800aa15..404bae346 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1299,13 +1299,9 @@ async def _check_for_energy_and_power_features( self._mac_in_str, states[NodeFeature.ENERGY], ) - - remaining_features: tuple[NodeFeature, ...] = () - for feature in features: - if feature in [NodeFeature.ENERGY, NodeFeature.POWER]: - continue - remaining_features += (feature,) - + remaining_features = tuple( + f for f in features if f not in {NodeFeature.ENERGY, NodeFeature.POWER} + ) return remaining_features, states return features, states From 373777ede4b07eabdff74c2a53181037d98a479c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 23 Jul 2025 13:08:59 +0200 Subject: [PATCH 11/14] Remove added debug-logging --- plugwise_usb/nodes/circle.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 404bae346..ef710aaa1 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -294,7 +294,6 @@ async def power_update(self) -> PowerStatistics | None: Return power usage or None if retrieval failed """ - _LOGGER.debug("Requesting a PowerStatistics update for %s", self._mac_in_str) # Debounce power if self.skip_update(self._power, MINIMAL_POWER_UPDATE): return self._power @@ -350,7 +349,6 @@ def _log_no_energy_stats_update(self) -> None: @raise_calibration_missing async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR0912 """Return updated energy usage statistics.""" - _LOGGER.debug("Requesting an EnergyStatistics update for %s", self._mac_in_str) if self._current_log_address is None: _LOGGER.debug( "Unable to update energy logs for node %s because the current log address is unknown.", @@ -371,11 +369,6 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09 self._current_log_address ) - _LOGGER.debug( - "Rollover status for %s: %s", - self._mac_in_str, - self._energy_counters.log_rollover, - ) if self._energy_counters.log_rollover: # Try updating node_info to collect the updated energy log address if await self.node_info_update() is None: From 8f0747995be740413f5a9e23f1c1098b7ea70862 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 23 Jul 2025 13:18:22 +0200 Subject: [PATCH 12/14] Shorten if and if construct --- 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 ef710aaa1..317019bcf 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1279,7 +1279,7 @@ async def _check_for_energy_and_power_features( If both are present, execute the related functions in a specific order to assure a proper response to the hourly pulses-rollovers. """ - if NodeFeature.ENERGY in features and NodeFeature.POWER in features: + if {NodeFeature.ENERGY, NodeFeature.POWER} <= features: states[NodeFeature.POWER] = await self.power_update() _LOGGER.debug( "async_get_state %s - power: %s", From 5d976788616c98d97d3cd4cca36c3591e1ba4452 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 23 Jul 2025 13:24:36 +0200 Subject: [PATCH 13/14] Shorten method removing NodeFeature.ENERGY/POWER --- plugwise_usb/nodes/circle.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 317019bcf..2a1fc58e6 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -1279,7 +1279,7 @@ async def _check_for_energy_and_power_features( If both are present, execute the related functions in a specific order to assure a proper response to the hourly pulses-rollovers. """ - if {NodeFeature.ENERGY, NodeFeature.POWER} <= features: + if {NodeFeature.ENERGY, NodeFeature.POWER} <= set(features): states[NodeFeature.POWER] = await self.power_update() _LOGGER.debug( "async_get_state %s - power: %s", @@ -1292,10 +1292,9 @@ async def _check_for_energy_and_power_features( self._mac_in_str, states[NodeFeature.ENERGY], ) - remaining_features = tuple( - f for f in features if f not in {NodeFeature.ENERGY, NodeFeature.POWER} - ) - return remaining_features, states + return tuple( + set(features).difference({NodeFeature.ENERGY, NodeFeature.POWER}) + ), states return features, states From cccab0b02f064d20b1b5bbb8c95be951eeb2f29f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 23 Jul 2025 16:08:04 +0200 Subject: [PATCH 14/14] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5e947b9..a5fccb90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## ONGOING +- Fix for [#291](https://github.com/plugwise/plugwise_usb-beta/issues/291) via PR [297](https://github.com/plugwise/python-plugwise-usb/pull/297) - PR [295](https://github.com/plugwise/python-plugwise-usb/pull/295): Streamline of loading function, to allow nodes to load even if temporarily offline, especially for SED nodes. ## v0.44.8