From f556da3bece946ffb4c08b820574209999d2ffe8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 08:50:40 +0200 Subject: [PATCH 01/40] Network: add set_measure_interval() Allows to enable/disable production measurements --- plugwise_usb/network/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 9a5a42b2f..20cf52ca6 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -14,7 +14,11 @@ from ..connection import StickController from ..constants import UTF8 from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout -from ..messages.requests import CirclePlusAllowJoiningRequest, NodePingRequest +from ..messages.requests import ( + CirclePlusAllowJoiningRequest, + CircleMeasureIntervalRequest, + NodePingRequest, +) from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, NODE_JOIN_ID, @@ -537,6 +541,17 @@ async def allow_join_requests(self, state: bool) -> None: _LOGGER.debug("Sent AllowJoiningRequest to Circle+ with state=%s", state) self.accept_join_request = state + async def set_measure_interval(self, consumption: int, production: int) -> None: + """Set the measure intervals for both consumption and production. + + Default: consumption = 60, production = 0. + For measuring in both directions set both to 60. + """ + _LOGGER.debug("set_measure_interval | cons=%s, prod=%s", consumption, production) + request = CircleMeasureIntervalRequest(self, consumption, production) + if (response := await request.send()) is None: + raise NodeError("No response for CircleMeasureIntervalRequest.") + def subscribe_to_node_events( self, node_event_callback: Callable[[NodeEvent, str], Coroutine[Any, Any, None]], From 0c5a22578641327d1333db5d092627f7fcff98be Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 12:43:43 +0200 Subject: [PATCH 02/40] Create corresponding function at __init__-level --- plugwise_usb/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 2e37b5be0..af2fe477c 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -210,6 +210,14 @@ async def set_accept_join_request(self, state: bool) -> bool: raise NodeError(f"Failed setting accept joining: {exc}") from exc return True + async def set_measure_interval(self, cons: int, prod: int) -> bool: + """Configure the measurement interval settings.""" + try: + await self._network.set_measure_interval(cons, prod) + except NodeError as exc: + raise NodeError(f"{exc}") + return True + async def clear_cache(self) -> None: """Clear current cache.""" if self._network is not None: From 215fdbe285c4032c12e5a08c0a3be0df7afb582d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 12:54:11 +0200 Subject: [PATCH 03/40] Fix missing send-callback --- plugwise_usb/network/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 20cf52ca6..00d302e70 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -548,7 +548,9 @@ async def set_measure_interval(self, consumption: int, production: int) -> None: For measuring in both directions set both to 60. """ _LOGGER.debug("set_measure_interval | cons=%s, prod=%s", consumption, production) - request = CircleMeasureIntervalRequest(self, consumption, production) + request = CircleMeasureIntervalRequest( + self._controller.send, consumption, production + ) if (response := await request.send()) is None: raise NodeError("No response for CircleMeasureIntervalRequest.") From 1d4e5aa9db3cc63e9f6f52e1618d198bd74d7f12 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 13:06:16 +0200 Subject: [PATCH 04/40] Add missing mac function-property --- plugwise_usb/__init__.py | 4 ++-- plugwise_usb/network/__init__.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index af2fe477c..65306e388 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -210,10 +210,10 @@ async def set_accept_join_request(self, state: bool) -> bool: raise NodeError(f"Failed setting accept joining: {exc}") from exc return True - async def set_measure_interval(self, cons: int, prod: int) -> bool: + async def set_measure_interval(self, mac: str, cons: int, prod: int) -> bool: """Configure the measurement interval settings.""" try: - await self._network.set_measure_interval(cons, prod) + await self._network.set_measure_interval(mac, cons, prod) except NodeError as exc: raise NodeError(f"{exc}") return True diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 00d302e70..807caade8 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -541,7 +541,9 @@ async def allow_join_requests(self, state: bool) -> None: _LOGGER.debug("Sent AllowJoiningRequest to Circle+ with state=%s", state) self.accept_join_request = state - async def set_measure_interval(self, consumption: int, production: int) -> None: + async def set_measure_interval( + self, mac: str, consumption: int, production: int + ) -> None: """Set the measure intervals for both consumption and production. Default: consumption = 60, production = 0. @@ -549,7 +551,7 @@ async def set_measure_interval(self, consumption: int, production: int) -> None: """ _LOGGER.debug("set_measure_interval | cons=%s, prod=%s", consumption, production) request = CircleMeasureIntervalRequest( - self._controller.send, consumption, production + self._controller.send, mac, consumption, production ) if (response := await request.send()) is None: raise NodeError("No response for CircleMeasureIntervalRequest.") From e38ef99f9bfe5f752a903dae815b2879c48b8f79 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 13:19:34 +0200 Subject: [PATCH 05/40] Add send-function to CircleMeasureIntervalRequest --- plugwise_usb/messages/requests.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 818c8c71e..3f9e49c26 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -1264,7 +1264,7 @@ class CircleMeasureIntervalRequest(PlugwiseRequest): FIXME: Make sure production interval is a multiply of consumption !! - Response message: Ack message with ??? TODO: + Response message: NodeResponse with ack-type POWER_LOG_INTERVAL_ACCEPTED """ _identifier = b"0057" @@ -1281,6 +1281,17 @@ def __init__( self._args.append(Int(consumption, length=4)) self._args.append(Int(production, length=4)) + async def send(self) -> NodeResponse | None: + """Send request.""" + result = await self._send_request() + if isinstance(result, NodeResponse): + return result + if result is None: + return None + raise MessageError( + f"Invalid response message. Received {result.__class__.__name__}, expected NodeResponse" + ) + class NodeClearGroupMacRequest(PlugwiseRequest): """TODO: usage?. From cb15700104e16e19b5c19149a07ce906be432fa9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 13:26:12 +0200 Subject: [PATCH 06/40] Improve response processing --- plugwise_usb/network/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 807caade8..4bd704292 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -553,7 +553,9 @@ async def set_measure_interval( request = CircleMeasureIntervalRequest( self._controller.send, mac, consumption, production ) - if (response := await request.send()) is None: + response = await request.send() + _LOGGER.debug("set_measure_interval | cons=%s", response) + if response is None: raise NodeError("No response for CircleMeasureIntervalRequest.") def subscribe_to_node_events( From 820ea102da37da88c76c4da213bfa04837f4696d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 13:26:57 +0200 Subject: [PATCH 07/40] Fix ident --- plugwise_usb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 65306e388..3b13649b9 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -211,7 +211,7 @@ async def set_accept_join_request(self, state: bool) -> bool: return True async def set_measure_interval(self, mac: str, cons: int, prod: int) -> bool: - """Configure the measurement interval settings.""" + """Configure the measurement interval settings.""" try: await self._network.set_measure_interval(mac, cons, prod) except NodeError as exc: From 2dc80a086bf27720bf17ca2ea3fd4cae6aa74572 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 13:33:13 +0200 Subject: [PATCH 08/40] Bump to v0.41.0a0 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80e822057..14cdc4c88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.40.1b1" +version = "v0.41.0a0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 6c784ea5cb9dfae1f950e836bb28479564502754 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 19:08:21 +0200 Subject: [PATCH 09/40] Pass mac as bytes --- 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 4bd704292..484f47525 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -551,7 +551,7 @@ async def set_measure_interval( """ _LOGGER.debug("set_measure_interval | cons=%s, prod=%s", consumption, production) request = CircleMeasureIntervalRequest( - self._controller.send, mac, consumption, production + self._controller.send, bytes(mac, UTF8), consumption, production ) response = await request.send() _LOGGER.debug("set_measure_interval | cons=%s", response) From baf59d1a723964fa9c85bce9aa614b6c85cd780f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 19:11:52 +0200 Subject: [PATCH 10/40] Bump to a1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 14cdc4c88..07f6d8526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.41.0a0" +version = "v0.41.0a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From c2e552eeea512d2529fc9e998027cf2817131000 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 19:48:15 +0200 Subject: [PATCH 11/40] Finish set_measurement_interval() function --- plugwise_usb/network/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 484f47525..8f5b8c66e 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -553,11 +553,14 @@ async def set_measure_interval( request = CircleMeasureIntervalRequest( self._controller.send, bytes(mac, UTF8), consumption, production ) - response = await request.send() - _LOGGER.debug("set_measure_interval | cons=%s", response) - if response is None: + 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" + ) + def subscribe_to_node_events( self, node_event_callback: Callable[[NodeEvent, str], Coroutine[Any, Any, None]], From bc2af7e4c3fc86661ccbcef2abfffd3d4514cc44 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 19:51:10 +0200 Subject: [PATCH 12/40] Update __ini__ - set_measure_interval() --- plugwise_usb/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 3b13649b9..846d20282 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -210,11 +210,13 @@ async def set_accept_join_request(self, state: bool) -> bool: raise NodeError(f"Failed setting accept joining: {exc}") from exc return True - async def set_measure_interval(self, mac: str, cons: int, prod: int) -> bool: + async def set_measure_interval( + self, mac: str, cons_interval: int, prod_interval: int + ) -> bool: """Configure the measurement interval settings.""" try: - await self._network.set_measure_interval(mac, cons, prod) - except NodeError as exc: + await self._network.set_measure_interval(mac, cons_interval, prod_interval) + except (MessageError, NodeError) as exc: raise NodeError(f"{exc}") return True From 826d3155c47115c1acc30ad0036386cf4fd89d86 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 19:54:36 +0200 Subject: [PATCH 13/40] Bump to a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 07f6d8526..9815f40c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.41.0a1" +version = "v0.41.0a2" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 76b50cd452e8dfac0ff39ed8d562c2d82131dccd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 26 May 2025 19:56:51 +0200 Subject: [PATCH 14/40] Update NodeResponseType list --- plugwise_usb/messages/responses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 16e6aa637..10104a986 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -56,9 +56,10 @@ class StickResponseType(bytes, Enum): class NodeResponseType(bytes, Enum): """Response types of a 'NodeResponse' reply message.""" - CIRCLE_PLUS = b"00DD" # type for CirclePlusAllowJoiningRequest with state false + CIRCLE_PLUS = b"00DD" # ack for CirclePlusAllowJoiningRequest with state false CLOCK_ACCEPTED = b"00D7" - JOIN_ACCEPTED = b"00D9" # type for CirclePlusAllowJoiningRequest with state true + JOIN_ACCEPTED = b"00D9" # ack for CirclePlusAllowJoiningRequest with state true + POWER_LOG_INTERVAL_ACCEPTED = b"00F8" # ack for CircleMeasureIntervalRequest RELAY_SWITCHED_OFF = b"00DE" RELAY_SWITCHED_ON = b"00D8" RELAY_SWITCH_FAILED = b"00E2" @@ -68,7 +69,6 @@ class NodeResponseType(bytes, Enum): # TODO: Validate these response types SED_CONFIG_FAILED = b"00F7" - POWER_LOG_INTERVAL_ACCEPTED = b"00F8" POWER_CALIBRATION_ACCEPTED = b"00DA" From e6b222508e5981a18b838584495e8bcf6fee1191 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 27 May 2025 19:53:12 +0200 Subject: [PATCH 15/40] Use a 2nd previous slot timestamp to detect returning to consumption only --- plugwise_usb/nodes/helpers/pulses.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 638e5bfeb..5cdc75dc7 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -506,7 +506,8 @@ def _update_log_direction( prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): - if self._logs[prev_address][prev_slot].timestamp == timestamp: + timestamp_2 = self._logs[prev_address][prev_slot].timestamp + if timestamp_2 == timestamp: # Given log is the second log with same timestamp, # mark direction as production self._logs[address][slot].is_consumption = False @@ -536,6 +537,13 @@ def _update_log_direction( elif self._log_production is None: self._log_production = False + prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) + if self._log_exists(prev_prev_address, prev_prev_slot): + timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp + self._log_production = ( + timestamp_2 == timestamp and timestamp_3 != timestamp + ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) + 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 ecfad81233ef66bb54c2c258f3b5d1b339f717a0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 27 May 2025 20:02:33 +0200 Subject: [PATCH 16/40] Move code-block up --- 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 5cdc75dc7..a599f83fd 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -521,6 +521,13 @@ def _update_log_direction( elif self._log_production is None: self._log_production = False + prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) + if self._log_exists(prev_prev_address, prev_prev_slot): + timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp + self._log_production = ( + timestamp_2 == timestamp and timestamp_3 != timestamp + ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) + next_address, next_slot = calc_log_address(address, slot, 1) if self._log_exists(next_address, next_slot): if self._logs[next_address][next_slot].timestamp == timestamp: @@ -537,13 +544,6 @@ def _update_log_direction( elif self._log_production is None: self._log_production = False - prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) - if self._log_exists(prev_prev_address, prev_prev_slot): - timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp - self._log_production = ( - timestamp_2 == timestamp and timestamp_3 != timestamp - ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) - 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 2fd033723ed1bd15604f78a430160b2e174498cb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 27 May 2025 20:05:33 +0200 Subject: [PATCH 17/40] Fix ident --- plugwise_usb/nodes/helpers/pulses.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index a599f83fd..1144e63fc 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -504,10 +504,11 @@ def _update_log_direction( if self._logs is None: return + prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): timestamp_2 = self._logs[prev_address][prev_slot].timestamp - if timestamp_2 == timestamp: + if timestamp_2 == timestamp: # Given log is the second log with same timestamp, # mark direction as production self._logs[address][slot].is_consumption = False @@ -521,12 +522,12 @@ def _update_log_direction( elif self._log_production is None: self._log_production = False - prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) - if self._log_exists(prev_prev_address, prev_prev_slot): - timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp - self._log_production = ( - timestamp_2 == timestamp and timestamp_3 != timestamp - ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) + prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) + if self._log_exists(prev_prev_address, prev_prev_slot): + timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp + self._log_production = ( + timestamp_2 == timestamp and timestamp_3 != timestamp + ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) next_address, next_slot = calc_log_address(address, slot, 1) if self._log_exists(next_address, next_slot): From 354e93e3d83dd7bc0dd7da4e5cf835227481c7e9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 27 May 2025 20:13:00 +0200 Subject: [PATCH 18/40] Add comment, clean up --- 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 1144e63fc..9c8f0ae85 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -504,7 +504,6 @@ def _update_log_direction( if self._logs is None: return - prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): timestamp_2 = self._logs[prev_address][prev_slot].timestamp @@ -525,6 +524,8 @@ def _update_log_direction( prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) if self._log_exists(prev_prev_address, prev_prev_slot): timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp + # _log_production is True when 2 out of 3 consecutive slots have + # the same timestamp, otherwise it is False self._log_production = ( timestamp_2 == timestamp and timestamp_3 != timestamp ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) @@ -538,7 +539,6 @@ def _update_log_direction( 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 From 78d57b2f23e45d9a86eafbfd3bdbd176314ff66c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 27 May 2025 20:24:33 +0200 Subject: [PATCH 19/40] Add debug logging --- plugwise_usb/nodes/helpers/pulses.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 9c8f0ae85..d06f5abf4 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -504,9 +504,21 @@ def _update_log_direction( if self._logs is None: return + _LOGGER.debug( + "_update_log_direction | address=%s | slot=%s | timestamp=%s", + address, + slot, + timestamp, + ) prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): timestamp_2 = self._logs[prev_address][prev_slot].timestamp + _LOGGER.debug( + "_update_log_direction | pr_address=%s | pr_slot=%s | timestamp=%s", + prev_address, + prev_slot, + timestamp_2, + ) if timestamp_2 == timestamp: # Given log is the second log with same timestamp, # mark direction as production @@ -524,6 +536,12 @@ def _update_log_direction( prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) if self._log_exists(prev_prev_address, prev_prev_slot): timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp + _LOGGER.debug( + "_update_log_direction | pr2_address=%s | pr2_slot=%s | timestamp=%s", + prev_prev_address, + prev_prev_slot, + timestamp_3, + ) # _log_production is True when 2 out of 3 consecutive slots have # the same timestamp, otherwise it is False self._log_production = ( @@ -532,7 +550,14 @@ def _update_log_direction( next_address, next_slot = calc_log_address(address, slot, 1) if self._log_exists(next_address, next_slot): - if self._logs[next_address][next_slot].timestamp == timestamp: + next_timestamp = self._logs[next_address][next_slot].timestamp + _LOGGER.debug( + "_update_log_direction | nxt_address=%s | nxt_slot=%s | timestamp=%s", + next_address, + next_slot, + next_timestamp, + ) + 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 From 38f41fb999fbd5cf7f85038351b40beb7b84bdec Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 08:18:07 +0200 Subject: [PATCH 20/40] Next try --- plugwise_usb/nodes/helpers/pulses.py | 32 ++++++++++++---------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index d06f5abf4..72c761d14 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -510,16 +510,18 @@ def _update_log_direction( slot, timestamp, ) + prev_exists = next_exists = False prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): - timestamp_2 = self._logs[prev_address][prev_slot].timestamp + prev_exists = True + prev_timestamp = self._logs[prev_address][prev_slot].timestamp _LOGGER.debug( "_update_log_direction | pr_address=%s | pr_slot=%s | timestamp=%s", prev_address, prev_slot, - timestamp_2, + prev_timestamp, ) - if timestamp_2 == timestamp: + if prev_timestamp == timestamp: # Given log is the second log with same timestamp, # mark direction as production self._logs[address][slot].is_consumption = False @@ -533,23 +535,9 @@ def _update_log_direction( elif self._log_production is None: self._log_production = False - prev_prev_address, prev_prev_slot = calc_log_address(address, slot, -2) - if self._log_exists(prev_prev_address, prev_prev_slot): - timestamp_3 = self._logs[prev_prev_address][prev_prev_slot].timestamp - _LOGGER.debug( - "_update_log_direction | pr2_address=%s | pr2_slot=%s | timestamp=%s", - prev_prev_address, - prev_prev_slot, - timestamp_3, - ) - # _log_production is True when 2 out of 3 consecutive slots have - # the same timestamp, otherwise it is False - self._log_production = ( - timestamp_2 == timestamp and timestamp_3 != timestamp - ) or (timestamp_2 == timestamp_3 and timestamp_2 != timestamp) - next_address, next_slot = calc_log_address(address, slot, 1) if self._log_exists(next_address, next_slot): + next_exists = True next_timestamp = self._logs[next_address][next_slot].timestamp _LOGGER.debug( "_update_log_direction | nxt_address=%s | nxt_slot=%s | timestamp=%s", @@ -564,12 +552,20 @@ def _update_log_direction( 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 + if prev_exists and next_exists: + # _log_production is True when 2 out of 3 consecutive slots have + # the same timestamp, otherwise it is False + self._log_production = ( + next_timestamp == timestamp and prev_timestamp != timestamp + ) or (next_timestamp == prev_timestamp and next_timestamp != timestamp) + 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 500c94465dd1a3706dba5cc07654f6d247174680 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 08:24:49 +0200 Subject: [PATCH 21/40] Full test-output --- scripts/tests_and_coverage.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 884a5221b..7e62cab10 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -23,7 +23,8 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/ + # PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || + PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/ fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 095923202275f6c8264ddabfe9c6de2287ebd05c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 08:30:49 +0200 Subject: [PATCH 22/40] Add direction to add_log() debug-logging --- plugwise_usb/nodes/helpers/pulses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 72c761d14..e829f44e6 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -439,11 +439,12 @@ def add_log( self.recalculate_missing_log_addresses() _LOGGER.debug( - "add_log | pulses=%s | address=%s | slot= %s |time:%s", + "add_log | pulses=%s | address=%s | slot=%s | time=%s, direction=%s", pulses, address, slot, timestamp, + direction, ) return True From 1aa15736a0639f385c39f75799e84865537ba418 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 08:34:09 +0200 Subject: [PATCH 23/40] Test: change to real production-numbers --- tests/test_usb.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 7a8542dc4..6f1de88f2 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1218,7 +1218,7 @@ def test_pulse_collection_production(self, monkeypatch: pytest.MonkeyPatch) -> N # Test consumption & production - Log import #1 - production # Missing addresses can not be determined yet test_timestamp = fixed_this_hour - td(hours=1) - tst_production.add_log(200, 2, test_timestamp, 2000) + tst_production.add_log(200, 2, test_timestamp, -2000) assert tst_production.log_addresses_missing is None assert tst_production.production_logging is None @@ -1226,7 +1226,7 @@ def test_pulse_collection_production(self, monkeypatch: pytest.MonkeyPatch) -> N # production must be enabled & intervals are unknown # Log at address 200 is known and expect production logs too test_timestamp = fixed_this_hour - td(hours=1) - tst_production.add_log(200, 1, test_timestamp, 1000) + tst_production.add_log(200, 1, test_timestamp, 0) assert tst_production.log_addresses_missing is None assert tst_production.log_interval_consumption is None assert tst_production.log_interval_production is None @@ -1235,7 +1235,7 @@ def test_pulse_collection_production(self, monkeypatch: pytest.MonkeyPatch) -> N # Test consumption & production - Log import #3 - production # Interval of consumption is not yet available test_timestamp = fixed_this_hour - td(hours=2) # type: ignore[unreachable] - tst_production.add_log(199, 4, test_timestamp, 4000) + tst_production.add_log(199, 4, test_timestamp, -2200) missing_check = list(range(199, 157, -1)) assert tst_production.log_addresses_missing == missing_check assert tst_production.log_interval_consumption is None @@ -1245,32 +1245,32 @@ def test_pulse_collection_production(self, monkeypatch: pytest.MonkeyPatch) -> N # Test consumption & production - Log import #4 # Interval of consumption is available test_timestamp = fixed_this_hour - td(hours=2) - tst_production.add_log(199, 3, test_timestamp, 3000) + tst_production.add_log(199, 3, test_timestamp, 0) assert tst_production.log_addresses_missing == missing_check assert tst_production.log_interval_consumption == 60 assert tst_production.log_interval_production == 60 assert tst_production.production_logging pulse_update_1 = fixed_this_hour + td(minutes=5) - tst_production.update_pulse_counter(100, 50, pulse_update_1) + tst_production.update_pulse_counter(0, -500, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour, is_consumption=True - ) == (100, pulse_update_1) + ) == (0, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour, is_consumption=False - ) == (50, pulse_update_1) + ) == (500, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour - td(hours=1), is_consumption=True - ) == (100, pulse_update_1) + ) == (0, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour - td(hours=2), is_consumption=True - ) == (1000 + 100, pulse_update_1) + ) == (0 + 0, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour - td(hours=1), is_consumption=False - ) == (50, pulse_update_1) + ) == (500, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour - td(hours=2), is_consumption=False - ) == (2000 + 50, pulse_update_1) + ) == (2000 + 500, pulse_update_1) _pulse_update = 0 From e49ce3849e24c0c0b09c347639ecfdac630fb2e8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 08:44:56 +0200 Subject: [PATCH 24/40] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9815f40c0..9cf9c060b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.41.0a2" +version = "v0.41.0a3" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 0a76ceae61104381028dcb5071d4c800ad30ce61 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 19:06:08 +0200 Subject: [PATCH 25/40] Improve --- plugwise_usb/nodes/helpers/pulses.py | 79 +++++++++++++++------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index e829f44e6..3dc69c027 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -91,6 +91,8 @@ def __init__(self, mac: str) -> None: self._logs: dict[int, dict[int, PulseLogRecord]] | None = None self._log_addresses_missing: list[int] | None = None self._log_production: bool | None = None + self._next_log_exists = False + self._prev_log_exists = False self._pulses_consumption: int | None = None self._pulses_production: int | None = None self._pulses_timestamp: datetime | None = None @@ -511,10 +513,8 @@ def _update_log_direction( slot, timestamp, ) - prev_exists = next_exists = False prev_address, prev_slot = calc_log_address(address, slot, -1) if self._log_exists(prev_address, prev_slot): - prev_exists = True prev_timestamp = self._logs[prev_address][prev_slot].timestamp _LOGGER.debug( "_update_log_direction | pr_address=%s | pr_slot=%s | timestamp=%s", @@ -522,23 +522,26 @@ def _update_log_direction( prev_slot, prev_timestamp, ) - 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 + if not self._prev_log_exists: + self._prev_log_exists = 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 + elif self._prev_log_exists: + self._prev_log_exists = False next_address, next_slot = calc_log_address(address, slot, 1) if self._log_exists(next_address, next_slot): - next_exists = True next_timestamp = self._logs[next_address][next_slot].timestamp _LOGGER.debug( "_update_log_direction | nxt_address=%s | nxt_slot=%s | timestamp=%s", @@ -546,26 +549,30 @@ def _update_log_direction( next_slot, next_timestamp, ) - 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 - - if prev_exists and next_exists: - # _log_production is True when 2 out of 3 consecutive slots have - # the same timestamp, otherwise it is False - self._log_production = ( - next_timestamp == timestamp and prev_timestamp != timestamp - ) or (next_timestamp == prev_timestamp and next_timestamp != timestamp) + if not self._next_log_exists: + self._next_log_exists = 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 + elif self._next_log_exists: + self._next_log_exists = False + + if self._prev_log_exists and self._next_log_exists: + # _log_production is True when 2 out of 3 consecutive slots have + # the same timestamp, otherwise it is False + self._log_production = ( + next_timestamp == timestamp and prev_timestamp != timestamp + ) or (next_timestamp == prev_timestamp and next_timestamp != timestamp) def _update_log_interval(self) -> None: """Update the detected log interval based on the most recent two logs.""" From 1972de8d3f9535e64f5afa71929b8f08c7b9cfd1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 28 May 2025 19:17:28 +0200 Subject: [PATCH 26/40] Bump to a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9cf9c060b..32dbfa9e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.41.0a3" +version = "v0.41.0a4" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 4b42a479d93f9d3f1c0cab51ac31afce015afd47 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:11:26 +0200 Subject: [PATCH 27/40] Correct function-names --- plugwise_usb/__init__.py | 4 ++-- plugwise_usb/network/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 846d20282..2fd9e71fe 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -210,12 +210,12 @@ async def set_accept_join_request(self, state: bool) -> bool: raise NodeError(f"Failed setting accept joining: {exc}") from exc return True - async def set_measure_interval( + async def set_measure_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: """Configure the measurement interval settings.""" try: - await self._network.set_measure_interval(mac, cons_interval, prod_interval) + await self._network.set_measure_intervals(mac, cons_interval, prod_interval) except (MessageError, NodeError) as exc: raise NodeError(f"{exc}") return True diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 8f5b8c66e..e14f1937e 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -541,7 +541,7 @@ async def allow_join_requests(self, state: bool) -> None: _LOGGER.debug("Sent AllowJoiningRequest to Circle+ with state=%s", state) self.accept_join_request = state - async def set_measure_interval( + async def set_measure_intervals( self, mac: str, consumption: int, production: int ) -> None: """Set the measure intervals for both consumption and production. @@ -549,7 +549,7 @@ async def set_measure_interval( Default: consumption = 60, production = 0. For measuring in both directions set both to 60. """ - _LOGGER.debug("set_measure_interval | cons=%s, prod=%s", consumption, production) + _LOGGER.debug("set_measure_intervals | cons=%s, prod=%s", consumption, production) request = CircleMeasureIntervalRequest( self._controller.send, bytes(mac, UTF8), consumption, production ) From 6efaa305dd7e75565f4b62576296016b0d4d6fc1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:14:12 +0200 Subject: [PATCH 28/40] Remove debug-logging used for testing --- plugwise_usb/nodes/helpers/pulses.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 3dc69c027..84f418ecf 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -507,21 +507,9 @@ def _update_log_direction( if self._logs is None: return - _LOGGER.debug( - "_update_log_direction | address=%s | slot=%s | timestamp=%s", - address, - slot, - timestamp, - ) 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 - _LOGGER.debug( - "_update_log_direction | pr_address=%s | pr_slot=%s | timestamp=%s", - prev_address, - prev_slot, - prev_timestamp, - ) if not self._prev_log_exists: self._prev_log_exists = True if prev_timestamp == timestamp: @@ -543,12 +531,6 @@ def _update_log_direction( 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 - _LOGGER.debug( - "_update_log_direction | nxt_address=%s | nxt_slot=%s | timestamp=%s", - next_address, - next_slot, - next_timestamp, - ) if not self._next_log_exists: self._next_log_exists = True if next_timestamp == timestamp: From 590b576d0d65f0f56f2dc1cc90e8601f83fb4c86 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:18:58 +0200 Subject: [PATCH 29/40] Improve function-names --- plugwise_usb/__init__.py | 6 +++--- plugwise_usb/network/__init__.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 2fd9e71fe..120199cf1 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -210,12 +210,12 @@ async def set_accept_join_request(self, state: bool) -> bool: raise NodeError(f"Failed setting accept joining: {exc}") from exc return True - async def set_measure_intervals( + async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: - """Configure the measurement interval settings.""" + """Configure the energy logging interval settings.""" try: - await self._network.set_measure_intervals(mac, cons_interval, prod_interval) + await self._network.set_energy_intervals(mac, cons_interval, prod_interval) except (MessageError, NodeError) as exc: raise NodeError(f"{exc}") return True diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index e14f1937e..accc8e4b5 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -541,15 +541,15 @@ async def allow_join_requests(self, state: bool) -> None: _LOGGER.debug("Sent AllowJoiningRequest to Circle+ with state=%s", state) self.accept_join_request = state - async def set_measure_intervals( + async def set_energy_intervals( self, mac: str, consumption: int, production: int ) -> None: - """Set the measure intervals for both consumption and production. + """Set the logging intervals for both energy consumption and production. Default: consumption = 60, production = 0. - For measuring in both directions set both to 60. + For logging energy in both directions set both to 60. """ - _LOGGER.debug("set_measure_intervals | cons=%s, prod=%s", consumption, production) + _LOGGER.debug("set_energy_intervals | cons=%s, prod=%s", consumption, production) request = CircleMeasureIntervalRequest( self._controller.send, bytes(mac, UTF8), consumption, production ) From d2be244e9fd70884014f2960c369cbaf0947d483 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:21:25 +0200 Subject: [PATCH 30/40] Fix --- plugwise_usb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 120199cf1..a69b830e1 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -217,7 +217,7 @@ async def set_energy_intervals( try: await self._network.set_energy_intervals(mac, cons_interval, prod_interval) except (MessageError, NodeError) as exc: - raise NodeError(f"{exc}") + raise NodeError(f"{exc}") from exc return True async def clear_cache(self) -> None: From e8869077019d5476b95add1c17e271b3d9041eea Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:26:18 +0200 Subject: [PATCH 31/40] Add input values checking, as suggested --- plugwise_usb/__init__.py | 2 +- plugwise_usb/network/__init__.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index a69b830e1..06222ab89 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -216,7 +216,7 @@ async def set_energy_intervals( """Configure the energy logging interval settings.""" try: await self._network.set_energy_intervals(mac, cons_interval, prod_interval) - except (MessageError, NodeError) as exc: + except (MessageError, NodeError, ValueError) as exc: raise NodeError(f"{exc}") from exc return True diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index accc8e4b5..5e9411a42 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -549,7 +549,15 @@ async def set_energy_intervals( Default: consumption = 60, production = 0. For logging energy in both directions set both to 60. """ - _LOGGER.debug("set_energy_intervals | cons=%s, prod=%s", consumption, production) + # Validate input parameters + if consumption <= 0: + raise ValueError("Consumption interval must be positive") + if production < 0: + raise ValueError("Production interval must be non-negative") + if production > 0 and production % consumption != 0: + raise ValueError("Production interval must be a multiple of consumption interval") + + _LOGGER.debug("set_energy_intervals | cons=%s, prod=%s", consumption, production) request = CircleMeasureIntervalRequest( self._controller.send, bytes(mac, UTF8), consumption, production ) From c611d6522508a4472152485c87b6048e2b02223f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:37:09 +0200 Subject: [PATCH 32/40] Remove blank space --- 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 84f418ecf..4b8a15aea 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -525,7 +525,7 @@ def _update_log_direction( self._reset_log_references() elif self._log_production is None: self._log_production = False - elif self._prev_log_exists: + elif self._prev_log_exists: self._prev_log_exists = False next_address, next_slot = calc_log_address(address, slot, 1) From 75aef63fa00dc7973b151216f5eba4303f94d670 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:42:52 +0200 Subject: [PATCH 33/40] Remove more blank spaces --- 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 5e9411a42..a9a163244 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -557,7 +557,7 @@ async def set_energy_intervals( if production > 0 and production % consumption != 0: raise ValueError("Production interval must be a multiple of consumption interval") - _LOGGER.debug("set_energy_intervals | cons=%s, prod=%s", consumption, production) + _LOGGER.debug("set_energy_intervals | cons=%s, prod=%s", consumption, production) request = CircleMeasureIntervalRequest( self._controller.send, bytes(mac, UTF8), consumption, production ) From 9f84d62403d0720ba327ddd25818907816847e74 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:51:32 +0200 Subject: [PATCH 34/40] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4035116aa..83072a67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.41.0 + +- Implement setting of energy logging intervals + ## v0.40.1 - Improve device Name and Model detection for Switch [#248](https://github.com/plugwise/python-plugwise-usb/pull/248) From eb9cbe5494e1dbdb72916aacda7f7930ac1f386e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 10:52:04 +0200 Subject: [PATCH 35/40] Set to v0.41.0 release-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 32dbfa9e5..7231b84db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.41.0a4" +version = "v0.41.0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From ef9fe0544e91de33b89b96673184300156d24202 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 11:32:52 +0200 Subject: [PATCH 36/40] Remove more blank spaces --- 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 a9a163244..cce2cea65 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -545,7 +545,7 @@ async def set_energy_intervals( self, mac: str, consumption: int, production: int ) -> None: """Set the logging intervals for both energy consumption and production. - + Default: consumption = 60, production = 0. For logging energy in both directions set both to 60. """ From 60efec58d4dcf6543d97981d4a65c62bf79e5613 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 14:58:02 +0200 Subject: [PATCH 37/40] Reduce complexity --- plugwise_usb/nodes/helpers/pulses.py | 51 ++++++++++++++++++---------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 4b8a15aea..a2e9ca960 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -88,11 +88,11 @@ def __init__(self, mac: str) -> None: self._rollover_consumption = False self._rollover_production = False + self._first_next_log_processed = False + self._first_prev_log_processed = False self._logs: dict[int, dict[int, PulseLogRecord]] | None = None self._log_addresses_missing: list[int] | None = None self._log_production: bool | None = None - self._next_log_exists = False - self._prev_log_exists = False self._pulses_consumption: int | None = None self._pulses_production: int | None = None self._pulses_timestamp: datetime | None = None @@ -377,7 +377,8 @@ def _detect_rollover( self._mac, direction ) - return False + + return False def add_empty_log(self, address: int, slot: int) -> None: """Add empty energy log record to mark any start of beginning of energy log collection.""" @@ -507,11 +508,24 @@ 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) + 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, otherwise it is False + self._log_production = ( + next_timestamp == timestamp and prev_timestamp != timestamp + ) or (next_timestamp == prev_timestamp and 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._prev_log_exists: - self._prev_log_exists = True + 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 @@ -525,14 +539,20 @@ def _update_log_direction( self._reset_log_references() elif self._log_production is None: self._log_production = False - elif self._prev_log_exists: - self._prev_log_exists = False + return prev_timestamp + elif 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._next_log_exists: - self._next_log_exists = True + 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 @@ -546,15 +566,10 @@ def _update_log_direction( self._logs[next_address][next_slot].is_consumption = True elif self._log_production is None: self._log_production = False - elif self._next_log_exists: - self._next_log_exists = False - - if self._prev_log_exists and self._next_log_exists: - # _log_production is True when 2 out of 3 consecutive slots have - # the same timestamp, otherwise it is False - self._log_production = ( - next_timestamp == timestamp and prev_timestamp != timestamp - ) or (next_timestamp == prev_timestamp and next_timestamp != timestamp) + return next_timestamp + elif 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.""" From d9aa981d72ac86c7efa37f22c6259cea395f98c4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 15:21:44 +0200 Subject: [PATCH 38/40] Fix --- plugwise_usb/nodes/helpers/pulses.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index a2e9ca960..d53665f64 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -377,7 +377,7 @@ def _detect_rollover( self._mac, direction ) - + return False def add_empty_log(self, address: int, slot: int) -> None: @@ -540,7 +540,8 @@ def _check_prev_production( elif self._log_production is None: self._log_production = False return prev_timestamp - elif self._first_prev_log_processed: + + if self._first_prev_log_processed: self._first_prev_log_processed = False return None @@ -567,7 +568,8 @@ def _check_next_production( elif self._log_production is None: self._log_production = False return next_timestamp - elif self._first_next_log_processed: + + if self._first_next_log_processed: self._first_next_log_processed = False return None From b46d61bac6fd2baac75181506a5061b6627812ea Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 30 May 2025 15:33:28 +0200 Subject: [PATCH 39/40] Revert return deletion --- plugwise_usb/nodes/helpers/pulses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index d53665f64..a5da42316 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -377,6 +377,7 @@ def _detect_rollover( self._mac, direction ) + return False return False From a969a315f7943a79a04c7824db08bdc5423ef708 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 31 May 2025 15:25:04 +0200 Subject: [PATCH 40/40] Update CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83072a67c..3e5f7d1ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## v0.41.0 -- Implement setting of energy logging intervals +- Implement setting of energy logging intervals [#247](https://github.com/plugwise/python-plugwise-usb/pull/247) -## v0.40.1 +## v0.40.1 (not released) - Improve device Name and Model detection for Switch [#248](https://github.com/plugwise/python-plugwise-usb/pull/248)