From 6eb6560fb40ce59845acd74abf2af831a3ce7b7e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Feb 2026 10:47:36 +0100 Subject: [PATCH 001/123] Add plus_pair_request function --- plugwise_usb/__init__.py | 8 ++++++++ plugwise_usb/network/__init__.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index e3cc2c6b3..eebcb9258 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -176,6 +176,14 @@ def port(self, port: str) -> None: self._port = port + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + try: + await self._network.pair_plus_device(mac) + except NodeError as exc: + raise NodeError(f"{exc}") from exc + return True + async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 985cd7581..707762495 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -152,6 +152,21 @@ def registry(self) -> list[str]: # endregion + aync def pair_plus_device(self, mac: str) -> bool: + """Register node to Plugwise network.""" + _LOGGER.debug("Pair Plus-device with mac: %s", mac) + if not validate_mac(mac): + raise NodeError(f"MAC {mac} invalid") + + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) + if (response := await request.send()) is None: + raise NodeError("No response for CirclePlusConnectRequest.") + + # how do we check for a succesfull pairing? + # there should be a 0005 wxyz 0001 response + # followed by a StickInitResponse (0011)? + + async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From 479ad722d2d5f94b4cdd1e51b52da24413daf3d8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:19:52 +0100 Subject: [PATCH 002/123] Document pairing process --- plugwise_usb/network/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 707762495..91f496889 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -153,7 +153,15 @@ def registry(self) -> list[str]: # endregion aync def pair_plus_device(self, mac: str) -> bool: - """Register node to Plugwise network.""" + """Register node to Plugwise network. + + According to https://roheve.wordpress.com/author/roheve/page/2/ + The pairing process should look like: + 0001 - 0002 + 000A - 0011 + 0004 - 0005 & 0061 (NodeJoinResponse) + The NodeJoinRespons should trigger... + """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): raise NodeError(f"MAC {mac} invalid") From 4d189ca4ceb2913345ba9f36e6f65e0c5a96455d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:43:00 +0100 Subject: [PATCH 003/123] Add 0001-0002 req-resp-pair --- plugwise_usb/network/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 91f496889..2beea9ae3 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -20,7 +20,11 @@ ) from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac -from ..messages.requests import CircleMeasureIntervalRequest, NodePingRequest +from ..messages.requests import ( + CircleMeasureIntervalRequest, + NodePingRequest, + StickNetworkInfoRequest, +) from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, NODE_JOIN_ID, @@ -30,6 +34,7 @@ NodeRejoinResponse, NodeResponseType, PlugwiseResponse, + StickNetworkInfoResponse, ) from ..nodes import get_plugwise_node from .registry import StickNetworkRegister @@ -166,6 +171,14 @@ def registry(self) -> list[str]: if not validate_mac(mac): raise NodeError(f"MAC {mac} invalid") + # Collect network info + request = StickNetworkInfoRequest(self._controller.send, None) + if (response := await request.send()) is not None: + if not isinstance(response, StickNetworkInfoResponse): + raise MessageError( + f"Invalid response message type ({response.__class__.__name__}) received, expected StickNetworkInfoResponse" + ) + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) if (response := await request.send()) is None: raise NodeError("No response for CirclePlusConnectRequest.") From f2db094c1b29e732f1c4959df2c6cb024b74f048 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:49:10 +0100 Subject: [PATCH 004/123] Add init stick --- plugwise_usb/network/__init__.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 2beea9ae3..335078958 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -173,12 +173,29 @@ def registry(self) -> list[str]: # Collect network info request = StickNetworkInfoRequest(self._controller.send, None) - if (response := await request.send()) is not None: - if not isinstance(response, StickNetworkInfoResponse): + if (info_response := await request.send()) is not None: + if not isinstance(info_response, StickNetworkInfoResponse): raise MessageError( - f"Invalid response message type ({response.__class__.__name__}) received, expected StickNetworkInfoResponse" + f"Invalid response message type ({info_response.__class__.__name__}) received, expected StickNetworkInfoResponse" ) + # Init Stick + try: + request = StickInitRequest(self._controller.send) + init_response: StickInitResponse | None = await request.send() + except StickError as err: + raise StickError( + "No response from USB-Stick to initialization request." + + " Validate USB-stick is connected to port " + + f"' {self._manager.serial_path}'" + ) from err + if init_response is None: + raise StickError( + "No response from USB-Stick to initialization request." + + " Validate USB-stick is connected to port " + + f"' {self._manager.serial_path}'" + ) + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) if (response := await request.send()) is None: raise NodeError("No response for CirclePlusConnectRequest.") From ceb6a23b0b744a941bb7deb667dfe5c4ac741666 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 18:42:54 +0100 Subject: [PATCH 005/123] Improve docstring --- plugwise_usb/network/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 335078958..603ae0a0d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -162,10 +162,10 @@ def registry(self) -> list[str]: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002 - 000A - 0011 - 0004 - 0005 & 0061 (NodeJoinResponse) - The NodeJoinRespons should trigger... + 0001 - 0002: StickNetworkInfoRequest - StickNetworkInfoResponse + 000A - 0011: StickInitRequest - StickInitResponse + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse + The Plus-device will then send a NodeRejoinResponse (0061). """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From b5688a5843cf1dbbaa94470e8c7e4aa9daf895db Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 19:29:26 +0100 Subject: [PATCH 006/123] Improve pair_plus_device() --- plugwise_usb/network/__init__.py | 49 +++++++++++++------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 603ae0a0d..72c8faf1d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -169,41 +169,32 @@ def registry(self) -> list[str]: """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): - raise NodeError(f"MAC {mac} invalid") + raise NodeError(f"Pairing failed: MAC {mac} invalid") # Collect network info - request = StickNetworkInfoRequest(self._controller.send, None) - if (info_response := await request.send()) is not None: - if not isinstance(info_response, StickNetworkInfoResponse): - raise MessageError( - f"Invalid response message type ({info_response.__class__.__name__}) received, expected StickNetworkInfoResponse" - ) + try: + request = StickNetworkInfoRequest(self._controller.send, None) + info_response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") + if info_response is None: + raise NodeError("Pairing failed, StickNetworkInfoResponse is None") # Init Stick try: - request = StickInitRequest(self._controller.send) - init_response: StickInitResponse | None = await request.send() - except StickError as err: - raise StickError( - "No response from USB-Stick to initialization request." - + " Validate USB-stick is connected to port " - + f"' {self._manager.serial_path}'" - ) from err - if init_response is None: - raise StickError( - "No response from USB-Stick to initialization request." - + " Validate USB-stick is connected to port " - + f"' {self._manager.serial_path}'" - ) - - request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - if (response := await request.send()) is None: - raise NodeError("No response for CirclePlusConnectRequest.") + await self._controller.initialize_stick() + except StickError as exc: + raise NodeError(f"Pairing failed, failed to initialize Stick: {exc}") - # how do we check for a succesfull pairing? - # there should be a 0005 wxyz 0001 response - # followed by a StickInitResponse (0011)? - + try: + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) + response = await request.send()) + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") + if response is None: + raise NodeError("Pairing failed, CirclePlusConnectResponse is None") + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" From 7a32b0b1e9dac2df1aa867df1f792792ded469c2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:11:42 +0100 Subject: [PATCH 007/123] Add todo for maybe needed functionality --- plugwise_usb/network/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 72c8faf1d..c7e888f82 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -166,6 +166,8 @@ def registry(self) -> list[str]: 000A - 0011: StickInitRequest - StickInitResponse 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse The Plus-device will then send a NodeRejoinResponse (0061). + + Todo(?): Does this need repeating until pairing is succesful? """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From 01b4222b3c933c5ef2aafbd081cbcc72d89a3326 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:23:34 +0100 Subject: [PATCH 008/123] Fix typos, return type --- plugwise_usb/network/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index c7e888f82..622034a2d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -157,7 +157,7 @@ def registry(self) -> list[str]: # endregion - aync def pair_plus_device(self, mac: str) -> bool: + async def pair_plus_device(self, mac: str) -> None: """Register node to Plugwise network. According to https://roheve.wordpress.com/author/roheve/page/2/ @@ -190,7 +190,7 @@ def registry(self) -> list[str]: try: request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - response = await request.send()) + response = await request.send() except MessageError as exc: raise NodeError(f"Pairing failed: {exc}") if response is None: From 2fe7fb4cc6dad417517dea10b7c81df0d04c0c72 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:28:33 +0100 Subject: [PATCH 009/123] Correct imports, improve docstring --- plugwise_usb/network/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 622034a2d..77176557e 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -21,6 +21,7 @@ from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac from ..messages.requests import ( + CirclePlusConnectRequest, CircleMeasureIntervalRequest, NodePingRequest, StickNetworkInfoRequest, @@ -34,7 +35,6 @@ NodeRejoinResponse, NodeResponseType, PlugwiseResponse, - StickNetworkInfoResponse, ) from ..nodes import get_plugwise_node from .registry import StickNetworkRegister @@ -158,8 +158,8 @@ def registry(self) -> list[str]: # endregion async def pair_plus_device(self, mac: str) -> None: - """Register node to Plugwise network. - + """Pair Plus-device to Plugwise Stick. + According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: 0001 - 0002: StickNetworkInfoRequest - StickNetworkInfoResponse From a06cd9bb55821e8d5863500791b7f8dbd4ce1a05 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:31:31 +0100 Subject: [PATCH 010/123] Ruff fixes --- plugwise_usb/network/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 77176557e..d55e1f5fc 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -21,8 +21,8 @@ from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac from ..messages.requests import ( - CirclePlusConnectRequest, CircleMeasureIntervalRequest, + CirclePlusConnectRequest, NodePingRequest, StickNetworkInfoRequest, ) @@ -178,23 +178,29 @@ async def pair_plus_device(self, mac: str) -> None: request = StickNetworkInfoRequest(self._controller.send, None) info_response = await request.send() except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") + raise NodeError(f"Pairing failed: {exc}") from exc if info_response is None: - raise NodeError("Pairing failed, StickNetworkInfoResponse is None") + raise NodeError( + "Pairing failed, StickNetworkInfoResponse is None" + ) from None # Init Stick try: await self._controller.initialize_stick() except StickError as exc: - raise NodeError(f"Pairing failed, failed to initialize Stick: {exc}") + raise NodeError( + f"Pairing failed, failed to initialize Stick: {exc}" + ) from exc try: request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) response = await request.send() except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") + raise NodeError(f"Pairing failed: {exc}") from exc if response is None: - raise NodeError("Pairing failed, CirclePlusConnectResponse is None") + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") From b95805a96401f6f6a5ac6fc34bf35018bb726280 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 8 Feb 2026 08:04:59 +0000 Subject: [PATCH 011/123] Make sure the Stick is ready to pair, as suggested Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- plugwise_usb/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index eebcb9258..dc3426142 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -176,8 +176,12 @@ def port(self, port: str) -> None: self._port = port + `@raise_not_connected` + `@raise_not_initialized` async def plus_pair_request(self, mac: str) -> bool: """Send a pair request to a Plus device.""" + if self._network is None: + raise StickError("Cannot pair when network is not initialized") try: await self._network.pair_plus_device(mac) except NodeError as exc: From 3a1c3cf3b3aa70b1f990b8b7efdcd053741987e6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:06:35 +0100 Subject: [PATCH 012/123] Fix spelling --- 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 d55e1f5fc..8c9b0e695 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -167,7 +167,7 @@ async def pair_plus_device(self, mac: str) -> None: 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse The Plus-device will then send a NodeRejoinResponse (0061). - Todo(?): Does this need repeating until pairing is succesful? + Todo(?): Does this need repeating until pairing is successful? """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From 747c8b9d20549b4aa7db7069db79bf93cbba9847 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:10:35 +0100 Subject: [PATCH 013/123] Remove quotes, move --- plugwise_usb/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index dc3426142..0d5e9040a 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -176,18 +176,6 @@ def port(self, port: str) -> None: self._port = port - `@raise_not_connected` - `@raise_not_initialized` - async def plus_pair_request(self, mac: str) -> bool: - """Send a pair request to a Plus device.""" - if self._network is None: - raise StickError("Cannot pair when network is not initialized") - try: - await self._network.pair_plus_device(mac) - except NodeError as exc: - raise NodeError(f"{exc}") from exc - return True - async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: @@ -290,6 +278,18 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() + @raise_not_connected + @raise_not_initialized + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + if self._network is None: + raise StickError("Cannot pair when network is not initialized") + try: + await self._network.pair_plus_device(mac) + except NodeError as exc: + raise NodeError(f"{exc}") from exc + return True + @raise_not_connected @raise_not_initialized async def start_network(self) -> None: From 2abb3b571f9f18dbbf760653212bd5903ea91d50 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:19:39 +0100 Subject: [PATCH 014/123] Set output as bool and use --- plugwise_usb/__init__.py | 7 ++----- plugwise_usb/network/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 0d5e9040a..6bac7da7d 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -284,11 +284,8 @@ async def plus_pair_request(self, mac: str) -> bool: """Send a pair request to a Plus device.""" if self._network is None: raise StickError("Cannot pair when network is not initialized") - try: - await self._network.pair_plus_device(mac) - except NodeError as exc: - raise NodeError(f"{exc}") from exc - return True + + return self._network.pair_plus_device(mac) @raise_not_connected @raise_not_initialized diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 8c9b0e695..a6ff22835 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -157,7 +157,7 @@ def registry(self) -> list[str]: # endregion - async def pair_plus_device(self, mac: str) -> None: + async def pair_plus_device(self, mac: str) -> bool: """Pair Plus-device to Plugwise Stick. According to https://roheve.wordpress.com/author/roheve/page/2/ @@ -204,6 +204,8 @@ async def pair_plus_device(self, mac: str) -> None: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") + return True + async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From ba1671069f5e2d265cd63eac20779f44dc92905d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:32:20 +0100 Subject: [PATCH 015/123] Add missing await --- 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 6bac7da7d..f61a87fe9 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -285,7 +285,7 @@ async def plus_pair_request(self, mac: str) -> bool: if self._network is None: raise StickError("Cannot pair when network is not initialized") - return self._network.pair_plus_device(mac) + return await self._network.pair_plus_device(mac) @raise_not_connected @raise_not_initialized From a72ae1043f1a6d9b3ba73cc7b8b5fea5b619f744 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:40:48 +0100 Subject: [PATCH 016/123] Add 0003 response to docstring --- plugwise_usb/network/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index a6ff22835..aedc486dd 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -162,10 +162,10 @@ async def pair_plus_device(self, mac: str) -> bool: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002: StickNetworkInfoRequest - StickNetworkInfoResponse - 000A - 0011: StickInitRequest - StickInitResponse - 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse - The Plus-device will then send a NodeRejoinResponse (0061). + 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), + 000A - 0011: StickInitRequest - StickInitResponse, + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, + the Plus-device will then send a NodeRejoinResponse (0061). Todo(?): Does this need repeating until pairing is successful? """ From 0de2788c204b673583ed555ddcc4cae793c1454f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:18:14 +0100 Subject: [PATCH 017/123] Start adding pairing test --- tests/stick_pair_data.py | 60 ++++++ tests/test_pairing.py | 396 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 tests/stick_pair_data.py create mode 100644 tests/test_pairing.py diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py new file mode 100644 index 000000000..186601fec --- /dev/null +++ b/tests/stick_pair_data.py @@ -0,0 +1,60 @@ +"""Plus-device pairing test data.""" + +RESPONSE_MESSAGES = { + b"\x05\x05\x03\x030001CAAB\r\n": ( + "Stick network info request", + b"000000C1", # Success ack + b"0002" # response msg_id + + b"0F" # channel + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"1606" + + b"01", + b"0003" # response msg_id + + b"00CE", # ? + ), + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"01" # network_is_online + + b"0098765432101234" # circle_plus_mac + + b"4321" # network_id + + b"FF", # unknown2 + ), + b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + "Pair request of plus-device 0098765432101234", + b"000000C1", # Success ack + b"0005" # response msg_id + + b"00" # existing + + b"01", # allowed + ), + b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( + "Node Info of stick 0123456789012345", + b"000000C1", # Success ack + b"0024" # msg_id + + b"0123456789012345" # mac + + b"00000000" # datetime + + b"00000000" # log address 0 + + b"00" # relay + + b"80" # hz + + b"653907008512" # hw_ver + + b"4E0843A9" # fw_ver + + b"00", # node_type (Stick) + ), +} + +FIRST_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"00", # network_is_offline + ), +} \ No newline at end of file diff --git a/tests/test_pairing.py b/tests/test_pairing.py new file mode 100644 index 000000000..96e48e83a --- /dev/null +++ b/tests/test_pairing.py @@ -0,0 +1,396 @@ +"""Test pairing plus-device to plugwise USB Stick.""" + +import asyncio +from collections.abc import Callable, Coroutine +from datetime import UTC, datetime as dt, timedelta as td +import importlib +import logging +import random +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +import pytest + +import aiofiles # type: ignore[import-untyped] +import crcmod +from freezegun import freeze_time + +crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000) + +pw_stick = importlib.import_module("plugwise_usb") +pw_api = importlib.import_module("plugwise_usb.api") +pw_exceptions = importlib.import_module("plugwise_usb.exceptions") +pw_connection = importlib.import_module("plugwise_usb.connection") +pw_connection_manager = importlib.import_module("plugwise_usb.connection.manager") +pw_constants = importlib.import_module("plugwise_usb.constants") +pw_helpers_cache = importlib.import_module("plugwise_usb.helpers.cache") +pw_network_cache = importlib.import_module("plugwise_usb.network.cache") +pw_node_cache = importlib.import_module("plugwise_usb.nodes.helpers.cache") +pw_receiver = importlib.import_module("plugwise_usb.connection.receiver") +pw_sender = importlib.import_module("plugwise_usb.connection.sender") +pw_requests = importlib.import_module("plugwise_usb.messages.requests") +pw_responses = importlib.import_module("plugwise_usb.messages.responses") +pw_msg_properties = importlib.import_module("plugwise_usb.messages.properties") +pw_userdata = importlib.import_module("stick_test_data") +pw_node = importlib.import_module("plugwise_usb.nodes.node") +pw_circle = importlib.import_module("plugwise_usb.nodes.circle") +pw_sed = importlib.import_module("plugwise_usb.nodes.sed") +pw_scan = importlib.import_module("plugwise_usb.nodes.scan") +pw_sense = importlib.import_module("plugwise_usb.nodes.sense") +pw_switch = importlib.import_module("plugwise_usb.nodes.switch") +pw_energy_counter = importlib.import_module("plugwise_usb.nodes.helpers.counter") +pw_energy_calibration = importlib.import_module("plugwise_usb.nodes.helpers") +pw_energy_pulses = importlib.import_module("plugwise_usb.nodes.helpers.pulses") + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + + +def inc_seq_id(seq_id: bytes | None) -> bytes: + """Increment sequence id.""" + if seq_id is None: + return b"0000" + temp_int = int(seq_id, 16) + 1 + if temp_int >= 65532: + temp_int = 0 + temp_str = str(hex(temp_int)).lstrip("0x").upper() + while len(temp_str) < 4: + temp_str = "0" + temp_str + return temp_str.encode() + + +def construct_message(data: bytes, seq_id: bytes = b"0000") -> bytes: + """Construct plugwise message.""" + body = data[:4] + seq_id + data[4:] + return bytes( + pw_constants.MESSAGE_HEADER + + body + + bytes(f"{crc_fun(body):04X}", pw_constants.UTF8) + + pw_constants.MESSAGE_FOOTER + ) + + +class DummyTransport: + """Dummy transport class.""" + + protocol_data_received: Callable[[bytes], None] + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + test_data: dict[bytes, tuple[str, bytes, bytes | None]] | None = None, + ) -> None: + """Initialize dummy transport class.""" + self._loop = loop + self._msg = 0 + self._seq_id = b"1233" + self._processed: list[bytes] = [] + self._first_response = test_data + self._second_response = test_data + if test_data is None: + self._first_response = pw_userdata.RESPONSE_MESSAGES + self._second_response = pw_userdata.SECOND_RESPONSE_MESSAGES + self.random_extra_byte = 0 + self._closing = False + + def is_closing(self) -> bool: + """Close connection.""" + return self._closing + + def write(self, data: bytes) -> None: + """Write data back to system.""" + log = None + ack = None + response = None + if data in self._processed and self._second_response is not None: + log, ack, response = self._second_response.get(data, (None, None, None)) + if log is None and self._first_response is not None: + log, ack, response = self._first_response.get(data, (None, None, None)) + if log is None: + resp = pw_userdata.PARTLY_RESPONSE_MESSAGES.get( + data[:24], (None, None, None) + ) + if resp is None: + _LOGGER.debug("No msg response for %s", str(data)) + return + log, ack, response = resp + if ack is None: + _LOGGER.debug("No ack response for %s", str(data)) + return + + self._seq_id = inc_seq_id(self._seq_id) + if response and self._msg == 0: + self.message_response_at_once(ack, response, self._seq_id) + self._processed.append(data) + else: + self.message_response(ack, self._seq_id) + self._processed.append(data) + if response is None or self._closing: + return + self._loop.create_task(self._delayed_response(response, self._seq_id)) + self._msg += 1 + + async def _delayed_response(self, data: bytes, seq_id: bytes) -> None: + delay = random.uniform(0.005, 0.025) + await asyncio.sleep(delay) + self.message_response(data, seq_id) + + def message_response(self, data: bytes, seq_id: bytes) -> None: + """Handle message response.""" + self.random_extra_byte += 1 + if self.random_extra_byte > 25: + self.protocol_data_received(b"\x83") + self.random_extra_byte = 0 + self.protocol_data_received(construct_message(data, seq_id) + b"\x83") + else: + self.protocol_data_received(construct_message(data, seq_id)) + + def message_response_at_once(self, ack: bytes, data: bytes, seq_id: bytes) -> None: + """Full message.""" + self.random_extra_byte += 1 + if self.random_extra_byte > 25: + self.protocol_data_received(b"\x83") + self.random_extra_byte = 0 + self.protocol_data_received( + construct_message(ack, seq_id) + + construct_message(data, seq_id) + + b"\x83" + ) + else: + self.protocol_data_received( + construct_message(ack, seq_id) + construct_message(data, seq_id) + ) + + def close(self) -> None: + """Close connection.""" + self._closing = True + + +class MockSerial: + """Mock serial connection.""" + + def __init__( + self, custom_response: dict[bytes, tuple[str, bytes, bytes | None]] | None + ) -> None: + """Init mocked serial connection.""" + self.custom_response = custom_response + self._protocol: pw_receiver.StickReceiver | None = None # type: ignore[name-defined] + self._transport: DummyTransport | None = None + + def inject_message(self, data: bytes, seq_id: bytes) -> None: + """Inject message to be received from stick.""" + if self._transport is None: + return + self._transport.message_response(data, seq_id) + + def trigger_connection_lost(self) -> None: + """Trigger connection lost.""" + if self._protocol is None: + return + self._protocol.connection_lost() + + async def mock_connection( + self, + loop: asyncio.AbstractEventLoop, + protocol_factory: Callable[[], pw_receiver.StickReceiver], # type: ignore[name-defined] + **kwargs: dict[str, Any], + ) -> tuple[DummyTransport, pw_receiver.StickReceiver]: # type: ignore[name-defined] + """Mock connection with dummy connection.""" + self._protocol = protocol_factory() + self._transport = DummyTransport(loop, self.custom_response) + self._transport.protocol_data_received = self._protocol.data_received + loop.call_soon_threadsafe(self._protocol.connection_made, self._transport) + return self._transport, self._protocol + + +class MockOsPath: + """Mock aiofiles.path class.""" + + async def exists(self, file_or_path: str) -> bool: # noqa: PLR0911 + """Exists folder.""" + test_exists = [ + "mock_folder_that_exists", + "mock_folder_that_exists/nodetype.cache", + "mock_folder_that_exists\\nodetype.cache", + "mock_folder_that_exists/0123456789ABCDEF.cache", + "mock_folder_that_exists\\0123456789ABCDEF.cache", + "mock_folder_that_exists\\file_that_exists.ext", + ] + if file_or_path in test_exists: + return True + return file_or_path == "mock_folder_that_exists/file_that_exists.ext" + + async def mkdir(self, path: str) -> None: + """Make dir.""" + return + + +class MockStickController: + """Mock stick controller.""" + + def __init__(self) -> None: + """Initialize MockStickController.""" + self.send_response: list[pw_responses.PlugwiseResponse] = [] + + async def subscribe_to_messages( + self, + node_response_callback: Callable[ # type: ignore[name-defined] + [pw_responses.PlugwiseResponse], Coroutine[Any, Any, bool] + ], + mac: bytes | None = None, + message_ids: tuple[bytes] | None = None, + ) -> Callable[[], None]: + """Subscribe a awaitable callback to be called when a specific message is received. + + Returns function to unsubscribe. + """ + + def dummy_method() -> None: + """Fake method.""" + + return dummy_method + + def append_response(self, response) -> None: + """Add response to queue.""" + self.send_response.append(response) + + def clear_responses(self) -> None: + """Clear response queue.""" + self.send_response.clear() + + async def send( + self, + request: pw_requests.PlugwiseRequest, # type: ignore[name-defined] + suppress_node_errors=True, + ) -> pw_responses.PlugwiseResponse | None: # type: ignore[name-defined] + """Submit request to queue and return response.""" + if self.send_response: + return self.send_response.pop(0) + return None + + +aiofiles.threadpool.wrap.register(MagicMock)( + lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) # pylint: disable=unnecessary-lambda +) + + +class TestStick: + """Test USB Stick.""" + + test_node_awake: asyncio.Future[str] + test_node_loaded: asyncio.Future[str] + test_node_join: asyncio.Future[str] + test_connected: asyncio.Future[bool] + test_disconnected: asyncio.Future[bool] + test_relay_state_on: asyncio.Future[bool] + test_relay_state_off: asyncio.Future[bool] + test_motion_on: asyncio.Future[bool] + test_motion_off: asyncio.Future[bool] + test_init_relay_state_off: asyncio.Future[bool] + test_init_relay_state_on: asyncio.Future[bool] + + async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> None: # type: ignore[name-defined] + """Callable dummy routine.""" + return + + @pytest.mark.asyncio + async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test connecting to stick.""" + monkeypatch.setattr( + pw_connection_manager, + "create_serial_connection", + MockSerial(None).mock_connection, + ) + stick = pw_stick.Stick(port="test_port", cache_enabled=False) + + unsub_connect = stick.subscribe_to_stick_events( + stick_event_callback=self.connected, + events=(pw_api.StickEvent.CONNECTED,), + ) + self.test_connected = asyncio.Future() + await stick.connect("test_port") + assert await self.test_connected + await stick.initialize() + assert stick.mac_stick == "0123456789012345" + assert stick.name == "Stick 12345" + assert stick.mac_coordinator == "0098765432101234" + assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) + assert stick.hardware == "070085" + assert not stick.network_discovered + assert stick.network_state + assert stick.network_id == 17185 + unsub_connect() + await stick.disconnect() + assert not stick.network_state + with pytest.raises(pw_exceptions.StickError): + stick.mac_stick + + @pytest.mark.asyncio + async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Testing Stick init without paired Circle.""" + mock_serial = MockSerial( + { + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # response msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"00", # network_is_offline + ), + } + ) + monkeypatch.setattr( + pw_connection_manager, + "create_serial_connection", + mock_serial.mock_connection, + ) + monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) + monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) + stick = pw_stick.Stick(port="test_port", cache_enabled=False) + await stick.connect() + with pytest.raises(pw_exceptions.StickError): + await stick.initialize() + await stick.disconnect() + +# async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] +# """Handle join event callback.""" +# if event == pw_api.NodeEvent.JOIN: +# self.test_node_join.set_result(mac) +# else: +# self.test_node_join.set_exception( +# BaseException( +# f"Invalid {event} event, expected " + f"{pw_api.NodeEvent.JOIN}" +# ) +# ) + + # @pytest.mark.asyncio + # async def test_stick_node_join_subscription( + # self, monkeypatch: pytest.MonkeyPatch + # ) -> None: + # """Testing "new_node" subscription.""" + # mock_serial = MockSerial(None) + # monkeypatch.setattr( + # pw_connection_manager, + # "create_serial_connection", + # mock_serial.mock_connection, + # ) + # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) + # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) + # stick = pw_stick.Stick("test_port", cache_enabled=False) + # await stick.connect() + # await stick.initialize() + # await stick.discover_nodes(load=False) + + # self.test_node_join = asyncio.Future() + # unusb_join = stick.subscribe_to_node_events( + # node_event_callback=self.node_join, + # events=(pw_api.NodeEvent.JOIN,), + # ) + + ## Inject NodeJoinAvailableResponse + # mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! + # mac_join_node = await self.test_node_join + # assert mac_join_node == "9999999999999999" + # unusb_join() + # await stick.disconnect() From a90dc54bc08381dd3fb9ae76afaf370b27f2e39f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:51:31 +0100 Subject: [PATCH 018/123] Add missing connected() --- tests/test_pairing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 96e48e83a..edbc12466 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -289,6 +289,13 @@ class TestStick: test_init_relay_state_off: asyncio.Future[bool] test_init_relay_state_on: asyncio.Future[bool] + async def connected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined] + """Set connected state helper.""" + if event is pw_api.StickEvent.CONNECTED: + self.test_connected.set_result(True) + else: + self.test_connected.set_exception(BaseException("Incorrect event")) + async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> None: # type: ignore[name-defined] """Callable dummy routine.""" return From 308a2e62efbc6f4638b2267129be7cd8669aee4f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:58:46 +0100 Subject: [PATCH 019/123] Add pairing-test --- tests/test_pairing.py | 116 +++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index edbc12466..233841bf7 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -300,64 +300,84 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No """Callable dummy routine.""" return +# @pytest.mark.asyncio +# async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: +# """Test connecting to stick.""" +# monkeypatch.setattr( +# pw_connection_manager, +# "create_serial_connection", +# MockSerial(None).mock_connection, +# ) +# stick = pw_stick.Stick(port="test_port", cache_enabled=False) + +# unsub_connect = stick.subscribe_to_stick_events( +# stick_event_callback=self.connected, +# events=(pw_api.StickEvent.CONNECTED,), +# ) +# self.test_connected = asyncio.Future() +# await stick.connect("test_port") +# assert await self.test_connected +# await stick.initialize() +# assert stick.mac_stick == "0123456789012345" +# assert stick.name == "Stick 12345" +# assert stick.mac_coordinator == "0098765432101234" +# assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) +# assert stick.hardware == "070085" +# assert not stick.network_discovered +# assert stick.network_state +# assert stick.network_id == 17185 +# unsub_connect() +# await stick.disconnect() +# assert not stick.network_state +# with pytest.raises(pw_exceptions.StickError): +# stick.mac_stick + +# @pytest.mark.asyncio +# async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: +# """Testing Stick init without paired Circle.""" +# mock_serial = MockSerial( +# { +# b"\x05\x05\x03\x03000AB43C\r\n": ( +# "STICK INIT", +# b"000000C1", # Success ack +# b"0011" # response msg_id +# + b"0123456789012345" # stick mac +# + b"00" # unknown1 +# + b"00", # network_is_offline +# ), +# } +# ) +# monkeypatch.setattr( +# pw_connection_manager, +# "create_serial_connection", +# mock_serial.mock_connection, +# ) +# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) +# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) +# stick = pw_stick.Stick(port="test_port", cache_enabled=False) +# await stick.connect() +# with pytest.raises(pw_exceptions.StickError): +# await stick.initialize() +# await stick.disconnect() + @pytest.mark.asyncio - async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test connecting to stick.""" + async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test pairing a plus-device.""" + mock_serial = MockSerial(None) monkeypatch.setattr( pw_connection_manager, "create_serial_connection", MockSerial(None).mock_connection, ) + monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) + monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) - - unsub_connect = stick.subscribe_to_stick_events( - stick_event_callback=self.connected, - events=(pw_api.StickEvent.CONNECTED,), - ) - self.test_connected = asyncio.Future() await stick.connect("test_port") - assert await self.test_connected await stick.initialize() - assert stick.mac_stick == "0123456789012345" - assert stick.name == "Stick 12345" - assert stick.mac_coordinator == "0098765432101234" - assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) - assert stick.hardware == "070085" - assert not stick.network_discovered - assert stick.network_state - assert stick.network_id == 17185 - unsub_connect() - await stick.disconnect() - assert not stick.network_state - with pytest.raises(pw_exceptions.StickError): - stick.mac_stick + + # Inject StickNetworkInfoRequest to trigger a pairing + mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! - @pytest.mark.asyncio - async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Testing Stick init without paired Circle.""" - mock_serial = MockSerial( - { - b"\x05\x05\x03\x03000AB43C\r\n": ( - "STICK INIT", - b"000000C1", # Success ack - b"0011" # response msg_id - + b"0123456789012345" # stick mac - + b"00" # unknown1 - + b"00", # network_is_offline - ), - } - ) - monkeypatch.setattr( - pw_connection_manager, - "create_serial_connection", - mock_serial.mock_connection, - ) - monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) - monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) - stick = pw_stick.Stick(port="test_port", cache_enabled=False) - await stick.connect() - with pytest.raises(pw_exceptions.StickError): - await stick.initialize() await stick.disconnect() # async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] From 2bbc735c30d5be9cfcaf88a47f4787ce4a00399b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:09:13 +0100 Subject: [PATCH 020/123] Link to stick_pair_data --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 233841bf7..2eb76ac66 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -31,7 +31,7 @@ pw_requests = importlib.import_module("plugwise_usb.messages.requests") pw_responses = importlib.import_module("plugwise_usb.messages.responses") pw_msg_properties = importlib.import_module("plugwise_usb.messages.properties") -pw_userdata = importlib.import_module("stick_test_data") +pw_userdata = importlib.import_module("stick_pair_data") pw_node = importlib.import_module("plugwise_usb.nodes.node") pw_circle = importlib.import_module("plugwise_usb.nodes.circle") pw_sed = importlib.import_module("plugwise_usb.nodes.sed") From 0d897635cc0cdd71432400e403232d8b4dc9e013 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:10:41 +0100 Subject: [PATCH 021/123] Fix stick_pair_data --- tests/stick_pair_data.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 186601fec..999822d24 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -48,13 +48,14 @@ ), } -FIRST_RESPONSE_MESSAGES = { - b"\x05\x05\x03\x03000AB43C\r\n": ( - "STICK INIT", +SECOND_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x03000D55555555555555555E46\r\n": ( + "ping reply for 5555555555555555", b"000000C1", # Success ack - b"0011" # msg_id - + b"0123456789012345" # stick mac - + b"00" # unknown1 - + b"00", # network_is_offline - ), -} \ No newline at end of file + b"000E" + + b"5555555555555555" # mac + + b"44" # rssi in + + b"33" # rssi out + + b"0055", # roundtrip + ) +} From 11983cab581bc57dfaaee6727b8741620ae32a18 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:12:44 +0100 Subject: [PATCH 022/123] Set network to offline --- tests/stick_pair_data.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 999822d24..d1db76ccd 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -21,10 +21,10 @@ b"0011" # msg_id + b"0123456789012345" # stick mac + b"00" # unknown1 - + b"01" # network_is_online - + b"0098765432101234" # circle_plus_mac - + b"4321" # network_id - + b"FF", # unknown2 + + b"00", # network_is_offline + # + b"0098765432101234" # circle_plus_mac + # + b"4321" # network_id + # + b"FF", # unknown2 ), b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", From 2927ca8cdd5756ffa9a5711a40f4027e1c6fef1f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:26:33 +0100 Subject: [PATCH 023/123] Try --- plugwise_usb/messages/responses.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 7be3cf599..aa9a29c44 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -434,16 +434,21 @@ def __init__(self) -> None: super().__init__(b"0011") self._unknown1 = Int(0, length=2) self._network_online = Int(0, length=2) - self._mac_nc = String(None, length=16) - self._network_id = Int(0, 4, False) - self._unknown2 = Int(0, length=2) self._params += [ self._unknown1, self._network_online, - self._mac_nc, - self._network_id, - self._unknown2, - ] + ] + if self._network_online == 1: + self._mac_nc = String(None, length=16) + self._network_id = Int(0, 4, False) + self._unknown2 = Int(0, length=2) + self._params += [ + self._unknown1, + self._network_online, + self._mac_nc, + self._network_id, + self._unknown2, + ] @property def mac_network_controller(self) -> str: From 6ae06a014d79fd18d9d883d532cdacc05863f155 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:35:33 +0100 Subject: [PATCH 024/123] Try 2 --- plugwise_usb/messages/responses.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index aa9a29c44..fd9adf378 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -438,17 +438,17 @@ def __init__(self) -> None: self._unknown1, self._network_online, ] - if self._network_online == 1: - self._mac_nc = String(None, length=16) - self._network_id = Int(0, 4, False) - self._unknown2 = Int(0, length=2) - self._params += [ - self._unknown1, - self._network_online, - self._mac_nc, - self._network_id, - self._unknown2, - ] +# if self._network_online == 1: +# self._mac_nc = String(None, length=16) +# self._network_id = Int(0, 4, False) +# self._unknown2 = Int(0, length=2) +# self._params += [ +# self._unknown1, +# self._network_online, +# self._mac_nc, +# self._network_id, +# self._unknown2, +# ] @property def mac_network_controller(self) -> str: From 64a818738df7f640d5f00b767d28d832096c437d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:41:24 +0100 Subject: [PATCH 025/123] Try 3 --- plugwise_usb/messages/responses.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index fd9adf378..985b64914 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -437,7 +437,9 @@ def __init__(self) -> None: self._params += [ self._unknown1, self._network_online, - ] + ] + self._mac_nc = None + self._network_id = None # if self._network_online == 1: # self._mac_nc = String(None, length=16) # self._network_id = Int(0, 4, False) @@ -451,14 +453,18 @@ def __init__(self) -> None: # ] @property - def mac_network_controller(self) -> str: + def mac_network_controller(self) -> str | None: """Return the mac of the network controller (Circle+).""" # Replace first 2 characters by 00 for mac of circle+ node + if self._mac_nc is None: + return None return "00" + self._mac_nc.value[2:] @property - def network_id(self) -> int: + def network_id(self) -> int | None: """Return network ID.""" + if self._network_id is None: + return None return self._network_id.value @property From 4e93f62b99d4d0cf79d1b55693ee65847dd36d1f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:16:16 +0100 Subject: [PATCH 026/123] Add StickInitShortResponse --- plugwise_usb/messages/responses.py | 75 +++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 985b64914..c42a68d70 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -417,8 +417,45 @@ def __init__(self, timestamp: datetime | None = None) -> None: self._params += [self.image_timestamp] +class StickInitShortResponse(PlugwiseResponse): + """Returns the configuration and status of the USB-Stick - no network. + + Supported protocols : 1.0, 2.0 + Response to request : StickInitRequest + """ + + def __init__(self) -> None: + """Initialize StickInitShortResponse message object.""" + super().__init__(b"0011") + self._unknown1 = Int(0, length=2) + self._network_online = Int(0, length=2) + self._params += [ + self._unknown1, + self._network_online, + ] + + @property + def mac_network_controller(self) -> str | None: + """Return the mac of the network controller (Circle+).""" + return None + + @property + def network_id(self) -> int | None: + """Return network ID.""" + return None + + @property + def network_online(self) -> bool: + """Return state of network.""" + return self._network_online.value == 1 + + def __repr__(self) -> str: + """Convert request into writable str.""" + return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" + + class StickInitResponse(PlugwiseResponse): - """Returns the configuration and status of the USB-Stick. + """Returns the configuration and status of the USB-Stick - network online. Optional: - circle_plus_mac @@ -434,37 +471,26 @@ def __init__(self) -> None: super().__init__(b"0011") self._unknown1 = Int(0, length=2) self._network_online = Int(0, length=2) + self._mac_nc = String(None, length=16) + self._network_id = Int(0, 4, False) + self._unknown2 = Int(0, length=2) self._params += [ self._unknown1, self._network_online, + self._mac_nc, + self._network_id, + self._unknown2, ] - self._mac_nc = None - self._network_id = None -# if self._network_online == 1: -# self._mac_nc = String(None, length=16) -# self._network_id = Int(0, 4, False) -# self._unknown2 = Int(0, length=2) -# self._params += [ -# self._unknown1, -# self._network_online, -# self._mac_nc, -# self._network_id, -# self._unknown2, -# ] @property - def mac_network_controller(self) -> str | None: + def mac_network_controller(self) -> str: """Return the mac of the network controller (Circle+).""" # Replace first 2 characters by 00 for mac of circle+ node - if self._mac_nc is None: - return None return "00" + self._mac_nc.value[2:] @property - def network_id(self) -> int | None: + def network_id(self) -> int: """Return network ID.""" - if self._network_id is None: - return None return self._network_id.value @property @@ -1011,8 +1037,15 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 return NodePingResponse() if identifier == b"0010": return NodeImageValidationResponse() + + # 0011 has two formats if identifier == b"0011": - return StickInitResponse() + if length == 20: + return StickInitShortResponse() + if length == 42: + return StickInitResponse() + return None + if identifier == b"0013": return CirclePowerUsageResponse() if identifier == b"0015": From 95e28e245517a27c3c128483503014fb56a5e6f9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:28:26 +0100 Subject: [PATCH 027/123] Remove commented-out in response --- tests/stick_pair_data.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index d1db76ccd..d1018f2a1 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -22,9 +22,6 @@ + b"0123456789012345" # stick mac + b"00" # unknown1 + b"00", # network_is_offline - # + b"0098765432101234" # circle_plus_mac - # + b"4321" # network_id - # + b"FF", # unknown2 ), b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", From 9443cc45fb344a1bee4a8957f1f3f916c27fa067 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:34:02 +0100 Subject: [PATCH 028/123] Update length --- plugwise_usb/messages/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index c42a68d70..abbbe59d4 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1040,9 +1040,9 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 # 0011 has two formats if identifier == b"0011": - if length == 20: + if length == 36: return StickInitShortResponse() - if length == 42: + if length == 60: return StickInitResponse() return None From b647719cfc41693bd220475ad0f0885adc889cfb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:41:44 +0100 Subject: [PATCH 029/123] Adapt StickInitRequest send() --- plugwise_usb/messages/requests.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 0f8c92f77..7d472c4a5 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -38,6 +38,7 @@ NodeSpecificResponse, PlugwiseResponse, StickInitResponse, + StickInitShortResponse, StickNetworkInfoResponse, StickResponse, StickResponseType, @@ -523,7 +524,7 @@ class StickInitRequest(PlugwiseRequest): """Initialize USB-Stick. Supported protocols : 1.0, 2.0 - Response message : StickInitResponse + Response message : StickInitResponse or StickInitShortResponse """ _identifier = b"000A" @@ -537,17 +538,17 @@ def __init__( super().__init__(send_fn, None) self._max_retries = 1 - async def send(self) -> StickInitResponse | None: + async def send(self) -> StickInitResponse | StickInitShortResponse | None: """Send request.""" if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, StickInitResponse): + if isinstance(result, StickInitResponse) or isinstance(result, StickInitShortResponse): return result if result is None: return None raise MessageError( - f"Invalid response message. Received {result.__class__.__name__}, expected StickInitResponse" + f"Invalid response message. Received {result.__class__.__name__}, expected StickInitResponse/StickInitShortResponse" ) From 0ca8607d58865d27af019abccb8f553d33090783 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:49:23 +0100 Subject: [PATCH 030/123] Update length StickInitResponse --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index abbbe59d4..800c80ae7 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1042,7 +1042,7 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 if identifier == b"0011": if length == 36: return StickInitShortResponse() - if length == 60: + if length == 58: return StickInitResponse() return None From b38bbbb006bf1f83ebb93331c0435bbb5759fc66 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:57:19 +0100 Subject: [PATCH 031/123] Clean up --- tests/stick_pair_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index d1018f2a1..195846adb 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -42,7 +42,7 @@ + b"653907008512" # hw_ver + b"4E0843A9" # fw_ver + b"00", # node_type (Stick) - ), + ), } SECOND_RESPONSE_MESSAGES = { From 978d7d6e2466d9d105b7fc6eb8b60febb396b0a1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:59:30 +0100 Subject: [PATCH 032/123] Full test-output - test_pairing --- 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 e3b7ee971..5c0ad8ac7 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,7 +56,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/test_pairing.py fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 010fad41ab2405984779bacecc20af86cd4af454 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:11:30 +0100 Subject: [PATCH 033/123] Allow init to fail --- tests/test_pairing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 2eb76ac66..502f40bc3 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -373,7 +373,8 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect("test_port") - await stick.initialize() + with pytest.raises(pw_exceptions.StickError): + await stick.initialize() # Inject StickNetworkInfoRequest to trigger a pairing mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! From bb9bbe3139e6da41a87e73699a3cd1e7fc702d9a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:13:36 +0100 Subject: [PATCH 034/123] Add sleep --- tests/test_pairing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 502f40bc3..8a202e274 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -376,6 +376,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: with pytest.raises(pw_exceptions.StickError): await stick.initialize() + await asyncio.sleep(5) # Inject StickNetworkInfoRequest to trigger a pairing mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! From 7563d82e5ffbbb09b61232729ba4d69260b65505 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:42:31 +0100 Subject: [PATCH 035/123] Call pair_plus_request() --- tests/test_pairing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 8a202e274..eb975b0e2 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -377,8 +377,8 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.initialize() await asyncio.sleep(5) - # Inject StickNetworkInfoRequest to trigger a pairing - mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! + await stick.plus_pair_request("0123456789012345") + await asyncio.sleep(5) await stick.disconnect() From d16175c9ac11fe313e5a7b5c29354edce369c766 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:46:12 +0100 Subject: [PATCH 036/123] Connected and initialized is not required --- plugwise_usb/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index f61a87fe9..2125368ca 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -278,13 +278,8 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() - @raise_not_connected - @raise_not_initialized async def plus_pair_request(self, mac: str) -> bool: """Send a pair request to a Plus device.""" - if self._network is None: - raise StickError("Cannot pair when network is not initialized") - return await self._network.pair_plus_device(mac) @raise_not_connected From 2636a59ccf3376a07b58b8a718e210a2d6f362f3 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sun, 8 Feb 2026 12:49:30 +0000 Subject: [PATCH 037/123] fixup: pair-plus Python code fixed using Ruff --- plugwise_usb/messages/requests.py | 4 +- tests/test_pairing.py | 181 +++++++++++++++--------------- 2 files changed, 94 insertions(+), 91 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 7d472c4a5..3c23c4a14 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -543,7 +543,9 @@ async def send(self) -> StickInitResponse | StickInitShortResponse | None: if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, StickInitResponse) or isinstance(result, StickInitShortResponse): + if isinstance(result, StickInitResponse) or isinstance( + result, StickInitShortResponse + ): return result if result is None: return None diff --git a/tests/test_pairing.py b/tests/test_pairing.py index eb975b0e2..33f09339c 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -300,65 +300,65 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No """Callable dummy routine.""" return -# @pytest.mark.asyncio -# async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: -# """Test connecting to stick.""" -# monkeypatch.setattr( -# pw_connection_manager, -# "create_serial_connection", -# MockSerial(None).mock_connection, -# ) -# stick = pw_stick.Stick(port="test_port", cache_enabled=False) - -# unsub_connect = stick.subscribe_to_stick_events( -# stick_event_callback=self.connected, -# events=(pw_api.StickEvent.CONNECTED,), -# ) -# self.test_connected = asyncio.Future() -# await stick.connect("test_port") -# assert await self.test_connected -# await stick.initialize() -# assert stick.mac_stick == "0123456789012345" -# assert stick.name == "Stick 12345" -# assert stick.mac_coordinator == "0098765432101234" -# assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) -# assert stick.hardware == "070085" -# assert not stick.network_discovered -# assert stick.network_state -# assert stick.network_id == 17185 -# unsub_connect() -# await stick.disconnect() -# assert not stick.network_state -# with pytest.raises(pw_exceptions.StickError): -# stick.mac_stick - -# @pytest.mark.asyncio -# async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: -# """Testing Stick init without paired Circle.""" -# mock_serial = MockSerial( -# { -# b"\x05\x05\x03\x03000AB43C\r\n": ( -# "STICK INIT", -# b"000000C1", # Success ack -# b"0011" # response msg_id -# + b"0123456789012345" # stick mac -# + b"00" # unknown1 -# + b"00", # network_is_offline -# ), -# } -# ) -# monkeypatch.setattr( -# pw_connection_manager, -# "create_serial_connection", -# mock_serial.mock_connection, -# ) -# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) -# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) -# stick = pw_stick.Stick(port="test_port", cache_enabled=False) -# await stick.connect() -# with pytest.raises(pw_exceptions.StickError): -# await stick.initialize() -# await stick.disconnect() + # @pytest.mark.asyncio + # async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: + # """Test connecting to stick.""" + # monkeypatch.setattr( + # pw_connection_manager, + # "create_serial_connection", + # MockSerial(None).mock_connection, + # ) + # stick = pw_stick.Stick(port="test_port", cache_enabled=False) + + # unsub_connect = stick.subscribe_to_stick_events( + # stick_event_callback=self.connected, + # events=(pw_api.StickEvent.CONNECTED,), + # ) + # self.test_connected = asyncio.Future() + # await stick.connect("test_port") + # assert await self.test_connected + # await stick.initialize() + # assert stick.mac_stick == "0123456789012345" + # assert stick.name == "Stick 12345" + # assert stick.mac_coordinator == "0098765432101234" + # assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) + # assert stick.hardware == "070085" + # assert not stick.network_discovered + # assert stick.network_state + # assert stick.network_id == 17185 + # unsub_connect() + # await stick.disconnect() + # assert not stick.network_state + # with pytest.raises(pw_exceptions.StickError): + # stick.mac_stick + + # @pytest.mark.asyncio + # async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: + # """Testing Stick init without paired Circle.""" + # mock_serial = MockSerial( + # { + # b"\x05\x05\x03\x03000AB43C\r\n": ( + # "STICK INIT", + # b"000000C1", # Success ack + # b"0011" # response msg_id + # + b"0123456789012345" # stick mac + # + b"00" # unknown1 + # + b"00", # network_is_offline + # ), + # } + # ) + # monkeypatch.setattr( + # pw_connection_manager, + # "create_serial_connection", + # mock_serial.mock_connection, + # ) + # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) + # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) + # stick = pw_stick.Stick(port="test_port", cache_enabled=False) + # await stick.connect() + # with pytest.raises(pw_exceptions.StickError): + # await stick.initialize() + # await stick.disconnect() @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: @@ -375,13 +375,14 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.connect("test_port") with pytest.raises(pw_exceptions.StickError): await stick.initialize() - + await asyncio.sleep(5) await stick.plus_pair_request("0123456789012345") await asyncio.sleep(5) await stick.disconnect() + # async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] # """Handle join event callback.""" # if event == pw_api.NodeEvent.JOIN: @@ -393,33 +394,33 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: # ) # ) - # @pytest.mark.asyncio - # async def test_stick_node_join_subscription( - # self, monkeypatch: pytest.MonkeyPatch - # ) -> None: - # """Testing "new_node" subscription.""" - # mock_serial = MockSerial(None) - # monkeypatch.setattr( - # pw_connection_manager, - # "create_serial_connection", - # mock_serial.mock_connection, - # ) - # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) - # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) - # stick = pw_stick.Stick("test_port", cache_enabled=False) - # await stick.connect() - # await stick.initialize() - # await stick.discover_nodes(load=False) - - # self.test_node_join = asyncio.Future() - # unusb_join = stick.subscribe_to_node_events( - # node_event_callback=self.node_join, - # events=(pw_api.NodeEvent.JOIN,), - # ) - - ## Inject NodeJoinAvailableResponse - # mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! - # mac_join_node = await self.test_node_join - # assert mac_join_node == "9999999999999999" - # unusb_join() - # await stick.disconnect() +# @pytest.mark.asyncio +# async def test_stick_node_join_subscription( +# self, monkeypatch: pytest.MonkeyPatch +# ) -> None: +# """Testing "new_node" subscription.""" +# mock_serial = MockSerial(None) +# monkeypatch.setattr( +# pw_connection_manager, +# "create_serial_connection", +# mock_serial.mock_connection, +# ) +# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) +# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) +# stick = pw_stick.Stick("test_port", cache_enabled=False) +# await stick.connect() +# await stick.initialize() +# await stick.discover_nodes(load=False) + +# self.test_node_join = asyncio.Future() +# unusb_join = stick.subscribe_to_node_events( +# node_event_callback=self.node_join, +# events=(pw_api.NodeEvent.JOIN,), +# ) + +## Inject NodeJoinAvailableResponse +# mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! +# mac_join_node = await self.test_node_join +# assert mac_join_node == "9999999999999999" +# unusb_join() +# await stick.disconnect() From 40b7cec64cdd12fa5c26869b0434fb214cf6e347 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:33:41 +0100 Subject: [PATCH 038/123] There can only be one response --- tests/stick_pair_data.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 195846adb..654ad2591 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -10,10 +10,8 @@ + b"0698765432101234" # 06 + plus-device mac + b"FFFFFFFFFFFFFFFF" + b"0698765432101234" # 06 + plus-device mac - + b"1606" - + b"01", - b"0003" # response msg_id - + b"00CE", # ? + + b"1606" # pan_id + + b"01", # index ), b"\x05\x05\x03\x03000AB43C\r\n": ( "STICK INIT", From f9f9cd4acb37d744d70e33f6a45f4cb505563a6f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:38:21 +0100 Subject: [PATCH 039/123] Use inheritance for StickInitResponse --- plugwise_usb/messages/responses.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 800c80ae7..9f620216e 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -454,7 +454,7 @@ def __repr__(self) -> str: return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" -class StickInitResponse(PlugwiseResponse): +class StickInitResponse(StickInitShortResponse): """Returns the configuration and status of the USB-Stick - network online. Optional: @@ -468,15 +468,11 @@ class StickInitResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize StickInitResponse message object.""" - super().__init__(b"0011") - self._unknown1 = Int(0, length=2) - self._network_online = Int(0, length=2) + super().__init__() self._mac_nc = String(None, length=16) self._network_id = Int(0, 4, False) self._unknown2 = Int(0, length=2) self._params += [ - self._unknown1, - self._network_online, self._mac_nc, self._network_id, self._unknown2, @@ -493,15 +489,6 @@ def network_id(self) -> int: """Return network ID.""" return self._network_id.value - @property - def network_online(self) -> bool: - """Return state of network.""" - return self._network_online.value == 1 - - def __repr__(self) -> str: - """Convert request into writable str.""" - return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" - class CirclePowerUsageResponse(PlugwiseResponse): """Returns power usage as impulse counters for several different time frames. From 4f58ef7195da7e6980212cf7185fdc6a65d7f53f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:52:34 +0100 Subject: [PATCH 040/123] Move pair_plus_device() to connection --- plugwise_usb/__init__.py | 8 ++--- plugwise_usb/connection/__init__.py | 53 ++++++++++++++++++++++++++++- plugwise_usb/network/__init__.py | 51 --------------------------- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 2125368ca..fa4de8144 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -266,6 +266,10 @@ async def connect(self, port: str | None = None) -> None: self._port, ) + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + return await self._controller.pair_plus_device(mac) + @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" @@ -278,10 +282,6 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() - async def plus_pair_request(self, mac: str) -> bool: - """Send a pair request to a Plus device.""" - return await self._network.pair_plus_device(mac) - @raise_not_connected @raise_not_initialized async def start_network(self) -> None: diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index ff17f1400..fd19b4e8f 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -9,12 +9,14 @@ from ..api import StickEvent from ..constants import UTF8 from ..exceptions import NodeError, StickError -from ..helpers.util import version_to_model +from ..helpers.util import validate_mac, version_to_model from ..messages.requests import ( + CirclePlusConnectRequest, NodeInfoRequest, NodePingRequest, PlugwiseRequest, StickInitRequest, + StickNetworkInfoRequest, ) from ..messages.responses import ( NodeInfoResponse, @@ -202,6 +204,55 @@ async def initialize_stick(self) -> None: if not self._network_online: raise StickError("Zigbee network connection to Circle+ is down.") + async def pair_plus_device(self, mac: str) -> bool: + """Pair Plus-device to Plugwise Stick. + + According to https://roheve.wordpress.com/author/roheve/page/2/ + The pairing process should look like: + 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), + 000A - 0011: StickInitRequest - StickInitResponse, + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, + the Plus-device will then send a NodeRejoinResponse (0061). + + Todo(?): Does this need repeating until pairing is successful? + """ + _LOGGER.debug("Pair Plus-device with mac: %s", mac) + if not validate_mac(mac): + raise NodeError(f"Pairing failed: MAC {mac} invalid") + + # Collect network info + try: + request = StickNetworkInfoRequest(self.send, None) + info_response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if info_response is None: + raise NodeError( + "Pairing failed, StickNetworkInfoResponse is None" + ) from None + + # Init Stick + try: + await self.initialize_stick() + except StickError as exc: + raise NodeError( + f"Pairing failed, failed to initialize Stick: {exc}" + ) from exc + + try: + request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if response is None: + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") + + return True + async def get_node_details( self, mac: str, ping_first: bool ) -> tuple[NodeInfoResponse | None, NodePingResponse | None]: diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index aedc486dd..50834a751 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -22,9 +22,7 @@ from ..helpers.util import validate_mac from ..messages.requests import ( CircleMeasureIntervalRequest, - CirclePlusConnectRequest, NodePingRequest, - StickNetworkInfoRequest, ) from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, @@ -157,55 +155,6 @@ def registry(self) -> list[str]: # endregion - async def pair_plus_device(self, mac: str) -> bool: - """Pair Plus-device to Plugwise Stick. - - According to https://roheve.wordpress.com/author/roheve/page/2/ - The pairing process should look like: - 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), - 000A - 0011: StickInitRequest - StickInitResponse, - 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, - the Plus-device will then send a NodeRejoinResponse (0061). - - Todo(?): Does this need repeating until pairing is successful? - """ - _LOGGER.debug("Pair Plus-device with mac: %s", mac) - if not validate_mac(mac): - raise NodeError(f"Pairing failed: MAC {mac} invalid") - - # Collect network info - try: - request = StickNetworkInfoRequest(self._controller.send, None) - info_response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if info_response is None: - raise NodeError( - "Pairing failed, StickNetworkInfoResponse is None" - ) from None - - # Init Stick - try: - await self._controller.initialize_stick() - except StickError as exc: - raise NodeError( - f"Pairing failed, failed to initialize Stick: {exc}" - ) from exc - - try: - request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if response is None: - raise NodeError( - "Pairing failed, CirclePlusConnectResponse is None" - ) from None - if response.allowed.value != 1: - raise NodeError("Pairing failed, not allowed") - - return True - async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From 4b9aa1897c9790b3c9f8e1246c6c418f18dc3229 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:03:58 +0100 Subject: [PATCH 041/123] Correct plus-mac --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 33f09339c..bb535be12 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -377,7 +377,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.initialize() await asyncio.sleep(5) - await stick.plus_pair_request("0123456789012345") + await stick.plus_pair_request("0098765432101234") await asyncio.sleep(5) await stick.disconnect() From a47e38df49fffdf9af35a4d5f6ab4ee96cd564d1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:08:47 +0100 Subject: [PATCH 042/123] Add stick-mac to 0002 response --- tests/stick_pair_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 654ad2591..d6d4ff947 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -5,6 +5,7 @@ "Stick network info request", b"000000C1", # Success ack b"0002" # response msg_id + + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" + b"0698765432101234" # 06 + plus-device mac From e46870c2c733131a275323a30c1a059b102cbe4c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:35:55 +0100 Subject: [PATCH 043/123] Move RESPONSE_MESSAGES --- tests/test_pairing.py | 110 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index bb535be12..64d900104 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -31,7 +31,6 @@ pw_requests = importlib.import_module("plugwise_usb.messages.requests") pw_responses = importlib.import_module("plugwise_usb.messages.responses") pw_msg_properties = importlib.import_module("plugwise_usb.messages.properties") -pw_userdata = importlib.import_module("stick_pair_data") pw_node = importlib.import_module("plugwise_usb.nodes.node") pw_circle = importlib.import_module("plugwise_usb.nodes.circle") pw_sed = importlib.import_module("plugwise_usb.nodes.sed") @@ -45,6 +44,61 @@ _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) +RESPONSE_MESSAGES = { + b"\x05\x05\x03\x030001CAAB\r\n": ( + "Stick network info request", + b"000000C1", # Success ack + b"0002" # response msg_id + + b"0123456789012345" # stick-mac + + b"0F" # channel + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"1606" # pan_id + + b"01", # index + ), + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"00", # network_is_offline + ), + b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + "Pair request of plus-device 0098765432101234", + b"000000C1", # Success ack + b"0005" # response msg_id + + b"00" # existing + + b"01", # allowed + ), + b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( + "Node Info of stick 0123456789012345", + b"000000C1", # Success ack + b"0024" # msg_id + + b"0123456789012345" # mac + + b"00000000" # datetime + + b"00000000" # log address 0 + + b"00" # relay + + b"80" # hz + + b"653907008512" # hw_ver + + b"4E0843A9" # fw_ver + + b"00", # node_type (Stick) + ), +} + +SECOND_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x03000D55555555555555555E46\r\n": ( + "ping reply for 5555555555555555", + b"000000C1", # Success ack + b"000E" + + b"5555555555555555" # mac + + b"44" # rssi in + + b"33" # rssi out + + b"0055", # roundtrip + ) +} def inc_seq_id(seq_id: bytes | None) -> bytes: """Increment sequence id.""" @@ -88,8 +142,8 @@ def __init__( self._first_response = test_data self._second_response = test_data if test_data is None: - self._first_response = pw_userdata.RESPONSE_MESSAGES - self._second_response = pw_userdata.SECOND_RESPONSE_MESSAGES + self._first_response = RESPONSE_MESSAGES + self._second_response = SECOND_RESPONSE_MESSAGES self.random_extra_byte = 0 self._closing = False @@ -107,7 +161,7 @@ def write(self, data: bytes) -> None: if log is None and self._first_response is not None: log, ack, response = self._first_response.get(data, (None, None, None)) if log is None: - resp = pw_userdata.PARTLY_RESPONSE_MESSAGES.get( + resp = PARTLY_RESPONSE_MESSAGES.get( data[:24], (None, None, None) ) if resp is None: @@ -360,6 +414,54 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No # await stick.initialize() # await stick.disconnect() + RESPONSE_MESSAGES = { + b"\x05\x05\x03\x030001CAAB\r\n": ( + "Stick network info request", + b"000000C1", # Success ack + b"0002" # response msg_id + + b"0123456789012345" # stick-mac + + b"0F" # channel + + b"FFFFFFFFFFFFFFFF" + + b"FF98765432101234" # 06 + plus-device mac + + b"FFFFFFFFFFFFFFFF" + + b"FF98765432101234" # 06 + plus-device mac + + b"04FF" # pan_id + + b"01", # index + ), + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"01" # network_is_online + + b"FF98765432101234" + + b"04FF" + + b"FF", + ), + b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + "Pair request of plus-device 0098765432101234", + b"000000C1", # Success ack + b"0005" # response msg_id + + b"00" # existing + + b"01", # allowed + ), + b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( + "Node Info of stick 0123456789012345", + b"000000C1", # Success ack + b"0024" # msg_id + + b"0123456789012345" # mac + + b"00000000" # datetime + + b"00000000" # log address 0 + + b"00" # relay + + b"80" # hz + + b"653907008512" # hw_ver + + b"4E0843A9" # fw_ver + + b"00", # node_type (Stick) + ), + } + + @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" From 7be4d41851f79002726e5220fcf0ea40b9da0590 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:46:23 +0100 Subject: [PATCH 044/123] Don't test network down first --- tests/test_pairing.py | 67 +++++++------------------------------------ 1 file changed, 10 insertions(+), 57 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 64d900104..61504f148 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -52,10 +52,10 @@ + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" - + b"0698765432101234" # 06 + plus-device mac + + b"FF98765432101234" # 06 + plus-device mac + b"FFFFFFFFFFFFFFFF" - + b"0698765432101234" # 06 + plus-device mac - + b"1606" # pan_id + + b"FF98765432101234" # 06 + plus-device mac + + b"04FF" # pan_id + b"01", # index ), b"\x05\x05\x03\x03000AB43C\r\n": ( @@ -64,7 +64,10 @@ b"0011" # msg_id + b"0123456789012345" # stick mac + b"00" # unknown1 - + b"00", # network_is_offline + + b"01" # network_is_online + + b"FF98765432101234" + + b"04FF" + + b"FF", ), b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", @@ -414,54 +417,6 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No # await stick.initialize() # await stick.disconnect() - RESPONSE_MESSAGES = { - b"\x05\x05\x03\x030001CAAB\r\n": ( - "Stick network info request", - b"000000C1", # Success ack - b"0002" # response msg_id - + b"0123456789012345" # stick-mac - + b"0F" # channel - + b"FFFFFFFFFFFFFFFF" - + b"FF98765432101234" # 06 + plus-device mac - + b"FFFFFFFFFFFFFFFF" - + b"FF98765432101234" # 06 + plus-device mac - + b"04FF" # pan_id - + b"01", # index - ), - b"\x05\x05\x03\x03000AB43C\r\n": ( - "STICK INIT", - b"000000C1", # Success ack - b"0011" # msg_id - + b"0123456789012345" # stick mac - + b"00" # unknown1 - + b"01" # network_is_online - + b"FF98765432101234" - + b"04FF" - + b"FF", - ), - b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( - "Pair request of plus-device 0098765432101234", - b"000000C1", # Success ack - b"0005" # response msg_id - + b"00" # existing - + b"01", # allowed - ), - b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( - "Node Info of stick 0123456789012345", - b"000000C1", # Success ack - b"0024" # msg_id - + b"0123456789012345" # mac - + b"00000000" # datetime - + b"00000000" # log address 0 - + b"00" # relay - + b"80" # hz - + b"653907008512" # hw_ver - + b"4E0843A9" # fw_ver - + b"00", # node_type (Stick) - ), - } - - @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" @@ -474,11 +429,9 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) - await stick.connect("test_port") - with pytest.raises(pw_exceptions.StickError): - await stick.initialize() - - await asyncio.sleep(5) + # await stick.connect("test_port") + # with pytest.raises(pw_exceptions.StickError): + # await stick.initialize() await stick.plus_pair_request("0098765432101234") await asyncio.sleep(5) From 06f68d2c5f144f2930758b68167e1778c15d5776 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:47:25 +0100 Subject: [PATCH 045/123] Add missing import --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index fd19b4e8f..58be25c4f 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -8,7 +8,7 @@ from ..api import StickEvent from ..constants import UTF8 -from ..exceptions import NodeError, StickError +from ..exceptions import MessageError, NodeError, StickError from ..helpers.util import validate_mac, version_to_model from ..messages.requests import ( CirclePlusConnectRequest, From ebcf21ed4a1dafdf9e7616ef2523fecc7341bea4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:49:34 +0100 Subject: [PATCH 046/123] Connect first --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 61504f148..fa2f3428c 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -429,7 +429,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) - # await stick.connect("test_port") + await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): # await stick.initialize() await stick.plus_pair_request("0098765432101234") From 4c004725689ba7b46234f1a58249c9c836d192a2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:52:43 +0100 Subject: [PATCH 047/123] Try --- tests/test_pairing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index fa2f3428c..97f408505 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -431,9 +431,9 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): - # await stick.initialize() - await stick.plus_pair_request("0098765432101234") - await asyncio.sleep(5) + await stick.initialize() + # await stick.plus_pair_request("0098765432101234") + # await asyncio.sleep(5) await stick.disconnect() From 683c53cba9439be5fc53efa17b635c621f0d3bb2 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 9 Feb 2026 07:56:13 +0000 Subject: [PATCH 048/123] fixup: pair-plus Python code fixed using Ruff --- tests/test_pairing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 97f408505..889fbe063 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -103,6 +103,7 @@ ) } + def inc_seq_id(seq_id: bytes | None) -> bytes: """Increment sequence id.""" if seq_id is None: @@ -164,9 +165,7 @@ def write(self, data: bytes) -> None: if log is None and self._first_response is not None: log, ack, response = self._first_response.get(data, (None, None, None)) if log is None: - resp = PARTLY_RESPONSE_MESSAGES.get( - data[:24], (None, None, None) - ) + resp = PARTLY_RESPONSE_MESSAGES.get(data[:24], (None, None, None)) if resp is None: _LOGGER.debug("No msg response for %s", str(data)) return From 656f667589f34d296077e377ce98e69336d358e4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:08:32 +0100 Subject: [PATCH 049/123] Try 3 --- plugwise_usb/connection/__init__.py | 26 ++++++++++++++------------ tests/test_pairing.py | 6 ++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 58be25c4f..2df82314b 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -230,6 +230,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None + _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -238,18 +239,19 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - - try: - request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) - response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if response is None: - raise NodeError( - "Pairing failed, CirclePlusConnectResponse is None" - ) from None - if response.allowed.value != 1: - raise NodeError("Pairing failed, not allowed") + _LOGGER.debug("HOI Init done") + + #try: + # request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + # response = await request.send() + #except MessageError as exc: + # raise NodeError(f"Pairing failed: {exc}") from exc + #if response is None: + # raise NodeError( + # "Pairing failed, CirclePlusConnectResponse is None" + # ) from None + #if response.allowed.value != 1: + # raise NodeError("Pairing failed, not allowed") return True diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 889fbe063..9508b6dbd 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -431,8 +431,10 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - # await stick.plus_pair_request("0098765432101234") - # await asyncio.sleep(5) + + await asyncio.sleep(2) + await stick.plus_pair_request("0098765432101234") + await asyncio.sleep(2) await stick.disconnect() From cd35ea6781c40e543a161429575a0465d73a72a6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:15:32 +0100 Subject: [PATCH 050/123] Try 4 --- plugwise_usb/connection/__init__.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 2df82314b..507589cb0 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -241,17 +241,19 @@ async def pair_plus_device(self, mac: str) -> bool: ) from exc _LOGGER.debug("HOI Init done") - #try: - # request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) - # response = await request.send() - #except MessageError as exc: - # raise NodeError(f"Pairing failed: {exc}") from exc - #if response is None: - # raise NodeError( - # "Pairing failed, CirclePlusConnectResponse is None" - # ) from None - #if response.allowed.value != 1: - # raise NodeError("Pairing failed, not allowed") + try: + request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if response is None: + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") + + _LOGGER.debug("HOI PlusConnectRequest done") return True From 799527de76a68834cb1026f33b05aeb3f369dd63 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:27:31 +0100 Subject: [PATCH 051/123] Try not allowed --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 9508b6dbd..fbe9e0750 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -74,7 +74,7 @@ b"000000C1", # Success ack b"0005" # response msg_id + b"00" # existing - + b"01", # allowed + + b"00", # not allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From e84b3a109bc960a84b11f2c1a4ff49a7b8bd6c16 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:28:46 +0100 Subject: [PATCH 052/123] Extra bit --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index fbe9e0750..d1c12f9b8 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -74,7 +74,7 @@ b"000000C1", # Success ack b"0005" # response msg_id + b"00" # existing - + b"00", # not allowed + + b"000", # not allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From f06b3c7add1b610892392a307a709bdfe2b1b093 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:32:54 +0100 Subject: [PATCH 053/123] CirclePlusConnectReqyest: shorter args --- plugwise_usb/messages/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 3c23c4a14..0656e9cca 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,7 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"00000000000000000000" + args = b"000000000000000000" msg: bytes = self._identifier + args if self._mac is not None: msg += self._mac From 30076ae1292b19601be81c078946bafed56ecee5 Mon Sep 17 00:00:00 2001 From: autoruff Date: Tue, 10 Feb 2026 07:34:30 +0000 Subject: [PATCH 054/123] fixup: pair-plus Python code fixed using Ruff --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index d1c12f9b8..c4d74f255 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -432,7 +432,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - await asyncio.sleep(2) + await asyncio.sleep(2) await stick.plus_pair_request("0098765432101234") await asyncio.sleep(2) From 97944138bdee28fa948612f5837d5d97c4aa6639 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:04:26 +0100 Subject: [PATCH 055/123] Add stick-mac to 0005-response, remove extra bit --- tests/test_pairing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index c4d74f255..80c5a9513 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -73,8 +73,9 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id + + b"0123456789012345" # stick-mac + b"00" # existing - + b"000", # not allowed + + b"00", # not allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From d099e4e510e3dcc3090b19927ec98ee056504b58 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:10:31 +0100 Subject: [PATCH 056/123] Ruffed --- plugwise_usb/network/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 50834a751..985cd7581 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -20,10 +20,7 @@ ) from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac -from ..messages.requests import ( - CircleMeasureIntervalRequest, - NodePingRequest, -) +from ..messages.requests import CircleMeasureIntervalRequest, NodePingRequest from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, NODE_JOIN_ID, From 03516a2fada54258813de8265ffcd6f69fafd3b7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:31:16 +0100 Subject: [PATCH 057/123] Shorten args, must be length=16 --- plugwise_usb/messages/requests.py | 2 +- tests/test_pairing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 0656e9cca..484eefd2d 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,7 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"000000000000000000" + args = b"0000000000000000" msg: bytes = self._identifier + args if self._mac is not None: msg += self._mac diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 80c5a9513..4f219e749 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + b"\x05\x05\x03\x03000400000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From c8a5a95cf9757796b47b40221532f1ac399b0386 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:47:09 +0100 Subject: [PATCH 058/123] Add missing CRC, can be corrected later --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 4f219e749..d41969a3d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x03000400000000000000000098765432101234\r\n": ( + b"\x05\x05\x03\x03000400000000000000000098765432101234ABCD\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From d87e5055725e1c42b648764702984b20940a3e0c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 12 Feb 2026 19:55:47 +0100 Subject: [PATCH 059/123] Try --- plugwise_usb/messages/requests.py | 2 +- tests/test_pairing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 484eefd2d..3c23c4a14 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,7 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"0000000000000000" + args = b"00000000000000000000" msg: bytes = self._identifier + args if self._mac is not None: msg += self._mac diff --git a/tests/test_pairing.py b/tests/test_pairing.py index d41969a3d..61b7dc249 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x03000400000000000000000098765432101234ABCD\r\n": ( + b"\x05\x05\x03\x030004000000000000000000000098765432101234ABCD\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From de3b7c85a9dcfea9b258e832cedb6e2764299e93 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 07:59:09 +0100 Subject: [PATCH 060/123] Try 2 --- tests/test_pairing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 61b7dc249..370ad95af 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -433,9 +433,9 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - await asyncio.sleep(2) + await asyncio.sleep(0.2) await stick.plus_pair_request("0098765432101234") - await asyncio.sleep(2) + await asyncio.sleep(0.2) await stick.disconnect() From ad969c69985336b0a33ce58424d459bdc890b8b9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:03:48 +0100 Subject: [PATCH 061/123] Fixes --- tests/test_pairing.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 370ad95af..26a2fbc37 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -92,6 +92,29 @@ ), } +PARTLY_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x0300161111111111111111": ( + "Clock set 1111111111111111", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"1111111111111111", # msg_id, ClockAccepted, mac + ), + b"\x05\x05\x03\x0300162222222222222222": ( + "Clock set 2222222222222222", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"2222222222222222", # msg_id, ClockAccepted, mac + ), + b"\x05\x05\x03\x0300163333333333333333": ( + "Clock set 3333333333333333", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"3333333333333333", # msg_id, ClockAccepted, mac + ), + b"\x05\x05\x03\x0300164444444444444444": ( + "Clock set 4444444444444444", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"4444444444444444", # msg_id, ClockAccepted, mac + ), +} + SECOND_RESPONSE_MESSAGES = { b"\x05\x05\x03\x03000D55555555555555555E46\r\n": ( "ping reply for 5555555555555555", @@ -420,7 +443,7 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" - mock_serial = MockSerial(None) + # mock_serial = MockSerial(None) monkeypatch.setattr( pw_connection_manager, "create_serial_connection", From df4298a951e1160ff32811f7b9556527ced5958e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:05:23 +0100 Subject: [PATCH 062/123] Correct CRC --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 26a2fbc37..5e5342ee4 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x030004000000000000000000000098765432101234ABCD\r\n": ( + b"\x05\x05\x03\x0300040000000000000000000000987654321012344D73\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From 2f55b95b84739e6f7d3b686cac8ecb64a67c5ff1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:08:49 +0100 Subject: [PATCH 063/123] Try allowed --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 5e5342ee4..8f1a88000 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -75,7 +75,7 @@ b"0005" # response msg_id + b"0123456789012345" # stick-mac + b"00" # existing - + b"00", # not allowed + + b"01", # allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From d72085c0087d96e39f53cdeb7d89d951309ca82f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:35:20 +0100 Subject: [PATCH 064/123] Change to Circle+ mac in 0005-response --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 8f1a88000..582dee78d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -73,7 +73,7 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id - + b"0123456789012345" # stick-mac + + b"0098765432101234" # circle+ mac + b"00" # existing + b"01", # allowed ), From f43efe4a89122342ba32ba38f505969a1b7e6951 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:56:13 +0100 Subject: [PATCH 065/123] Ruff-cleanup --- tests/test_pairing.py | 110 +----------------------------------------- 1 file changed, 2 insertions(+), 108 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 582dee78d..bf1037497 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -2,18 +2,16 @@ import asyncio from collections.abc import Callable, Coroutine -from datetime import UTC, datetime as dt, timedelta as td import importlib import logging import random from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock import pytest import aiofiles # type: ignore[import-untyped] import crcmod -from freezegun import freeze_time crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000) @@ -350,7 +348,7 @@ async def send( aiofiles.threadpool.wrap.register(MagicMock)( - lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) # pylint: disable=unnecessary-lambda + lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) # noqa: PLW0108 pylint: disable=unnecessary-lambda ) @@ -380,66 +378,6 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No """Callable dummy routine.""" return - # @pytest.mark.asyncio - # async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: - # """Test connecting to stick.""" - # monkeypatch.setattr( - # pw_connection_manager, - # "create_serial_connection", - # MockSerial(None).mock_connection, - # ) - # stick = pw_stick.Stick(port="test_port", cache_enabled=False) - - # unsub_connect = stick.subscribe_to_stick_events( - # stick_event_callback=self.connected, - # events=(pw_api.StickEvent.CONNECTED,), - # ) - # self.test_connected = asyncio.Future() - # await stick.connect("test_port") - # assert await self.test_connected - # await stick.initialize() - # assert stick.mac_stick == "0123456789012345" - # assert stick.name == "Stick 12345" - # assert stick.mac_coordinator == "0098765432101234" - # assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) - # assert stick.hardware == "070085" - # assert not stick.network_discovered - # assert stick.network_state - # assert stick.network_id == 17185 - # unsub_connect() - # await stick.disconnect() - # assert not stick.network_state - # with pytest.raises(pw_exceptions.StickError): - # stick.mac_stick - - # @pytest.mark.asyncio - # async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: - # """Testing Stick init without paired Circle.""" - # mock_serial = MockSerial( - # { - # b"\x05\x05\x03\x03000AB43C\r\n": ( - # "STICK INIT", - # b"000000C1", # Success ack - # b"0011" # response msg_id - # + b"0123456789012345" # stick mac - # + b"00" # unknown1 - # + b"00", # network_is_offline - # ), - # } - # ) - # monkeypatch.setattr( - # pw_connection_manager, - # "create_serial_connection", - # mock_serial.mock_connection, - # ) - # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) - # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) - # stick = pw_stick.Stick(port="test_port", cache_enabled=False) - # await stick.connect() - # with pytest.raises(pw_exceptions.StickError): - # await stick.initialize() - # await stick.disconnect() - @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" @@ -453,7 +391,6 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect("test_port") - # with pytest.raises(pw_exceptions.StickError): await stick.initialize() await asyncio.sleep(0.2) @@ -461,46 +398,3 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await asyncio.sleep(0.2) await stick.disconnect() - - -# async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] -# """Handle join event callback.""" -# if event == pw_api.NodeEvent.JOIN: -# self.test_node_join.set_result(mac) -# else: -# self.test_node_join.set_exception( -# BaseException( -# f"Invalid {event} event, expected " + f"{pw_api.NodeEvent.JOIN}" -# ) -# ) - -# @pytest.mark.asyncio -# async def test_stick_node_join_subscription( -# self, monkeypatch: pytest.MonkeyPatch -# ) -> None: -# """Testing "new_node" subscription.""" -# mock_serial = MockSerial(None) -# monkeypatch.setattr( -# pw_connection_manager, -# "create_serial_connection", -# mock_serial.mock_connection, -# ) -# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) -# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) -# stick = pw_stick.Stick("test_port", cache_enabled=False) -# await stick.connect() -# await stick.initialize() -# await stick.discover_nodes(load=False) - -# self.test_node_join = asyncio.Future() -# unusb_join = stick.subscribe_to_node_events( -# node_event_callback=self.node_join, -# events=(pw_api.NodeEvent.JOIN,), -# ) - -## Inject NodeJoinAvailableResponse -# mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! -# mac_join_node = await self.test_node_join -# assert mac_join_node == "9999999999999999" -# unusb_join() -# await stick.disconnect() From a1fc1b9c221e2419184ae11b2b7e33e3a770a1ce Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:15:14 +0100 Subject: [PATCH 066/123] Bump to v0.48.0a1 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0379b7971..9665f9035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.47.8" +version = "0.48.0a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From b43245eb21ed874914f68af5bc4e0d1a35313405 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:17:07 +0100 Subject: [PATCH 067/123] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf26710c..1445a627d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ PR [422](https://github.com/plugwise/python-plugwise-usb/pull/422): Add missing - PR [418](https://github.com/plugwise/python-plugwise-usb/pull/418): Improve raise-message for better debugging - PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Fix recent Ruff errors +- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) ## v0.47.2 - 2026-01-29 - PR [400](https://github.com/plugwise/python-plugwise-usb/pull/400): Fix for Issue [#399](https://github.com/plugwise/python-plugwise-usb/issues/399) From d5d702ad75349b22eecdd921104383517ff544cc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:41:52 +0100 Subject: [PATCH 068/123] Implement StickInitShortResponse-handling in class StickController --- plugwise_usb/connection/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 507589cb0..77e8e1c06 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -23,6 +23,7 @@ NodePingResponse, PlugwiseResponse, StickInitResponse, + StickInitShortResponse, ) from .manager import StickConnectionManager from .queue import StickQueue @@ -172,7 +173,9 @@ async def initialize_stick(self) -> None: try: request = StickInitRequest(self.send) - init_response: StickInitResponse | None = await request.send() + init_response: ( + StickInitResponse | StickInitShortResponse | None + ) = await request.send() except StickError as err: raise StickError( "No response from USB-Stick to initialization request." @@ -188,10 +191,11 @@ async def initialize_stick(self) -> None: self._mac_stick = init_response.mac_decoded self.stick_name = f"Stick {self._mac_stick[-5:]}" self._network_online = init_response.network_online + if self._network_online: + # Replace first 2 characters by 00 for mac of circle+ node + self._mac_nc = init_response.mac_network_controller + self._network_id = init_response.network_id - # Replace first 2 characters by 00 for mac of circle+ node - self._mac_nc = init_response.mac_network_controller - self._network_id = init_response.network_id self._is_initialized = True # Add Stick NodeInfoRequest @@ -201,9 +205,6 @@ async def initialize_stick(self) -> None: hardware, _ = version_to_model(node_info.hardware) self._hw_stick = hardware - if not self._network_online: - raise StickError("Zigbee network connection to Circle+ is down.") - async def pair_plus_device(self, mac: str) -> bool: """Pair Plus-device to Plugwise Stick. From ea120d7b7fe86cc52e43012c8c80fa2c40c0ece0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:44:42 +0100 Subject: [PATCH 069/123] Back to full test-output --- scripts/tests_and_coverage.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 5c0ad8ac7..54fe2b020 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,8 +56,7 @@ 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/test_pairing.py + PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From a31d3a321654f41619f65ef6cddb5738075ee8b4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:22:04 +0100 Subject: [PATCH 070/123] Improve --- plugwise_usb/messages/requests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 3c23c4a14..b8a34b458 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -543,9 +543,7 @@ async def send(self) -> StickInitResponse | StickInitShortResponse | None: if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, StickInitResponse) or isinstance( - result, StickInitShortResponse - ): + if isinstance(result, (StickInitResponse, StickInitShortResponse)): return result if result is None: return None From d9d92f6214f76b3a650658bcd3b9619b44adeb9f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:28:28 +0100 Subject: [PATCH 071/123] Correct CHANGELOG after rebase --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1445a627d..464190c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,9 @@ PR [422](https://github.com/plugwise/python-plugwise-usb/pull/422): Add missing ## v0.47.3 - 2026-03-04 -- PR [418](https://github.com/plugwise/python-plugwise-usb/pull/418): Improve raise-message for better debugging -- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Fix recent Ruff errors - +- PR [409](https://github.com/plugwise/python-plugwise-usb/pull/409): Fix recent Ruff errors - PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) + ## v0.47.2 - 2026-01-29 - PR [400](https://github.com/plugwise/python-plugwise-usb/pull/400): Fix for Issue [#399](https://github.com/plugwise/python-plugwise-usb/issues/399) From e780ed0644c02cdccb3e3e0829a73e4f5b7cc0ca Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:29:04 +0100 Subject: [PATCH 072/123] Bump to v0.48.0a2 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9665f9035..24332a19f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a1" +version = "0.48.0a2" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 2d16dde5e076d153c4139f9e0afb60dde61d3cef Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:31:46 +0100 Subject: [PATCH 073/123] Run all test-files in case of failure --- scripts/tests_and_coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 54fe2b020..e3b7ee971 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,7 +56,7 @@ 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/test_pairing.py + 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 bff07354a80e96f17118850cd28e844ada93a628 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 10:45:38 +0100 Subject: [PATCH 074/123] Revert back to python 3.13 --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 710c318ff..da7ddc826 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -132,7 +132,7 @@ jobs: - commitcheck strategy: matrix: - python-version: ["3.14"] + python-version: ["3.13"] steps: - name: Check out committed code uses: actions/checkout@v7 From 3eeb4710651f0568e049ed523f648b662a8749f7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 11:03:36 +0100 Subject: [PATCH 075/123] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 24332a19f..6c01be5a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a2" +version = "0.48.0a3" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 28d912d3666e46ccbd8dcec868fe82741dcbdf35 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:33:49 +0100 Subject: [PATCH 076/123] Try-except stick-initialize --- plugwise_usb/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index fa4de8144..3259d960b 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -273,7 +273,11 @@ async def plus_pair_request(self, mac: str) -> bool: @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" - await self._controller.initialize_stick() + try: + await self._controller.initialize_stick() + except StickError as exc: + raise StickError(f"Cannot initialize Stick-connection: {exc}") from exc + if self._network is None: self._network = StickNetwork(self._controller) self._network.cache_folder = self._cache_folder From 1aedfa3abdd2ff2f8755aaff07f40422b1b109fb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:46:16 +0100 Subject: [PATCH 077/123] Ruff fix --- plugwise_usb/messages/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index b8a34b458..4b35a865d 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -543,7 +543,7 @@ async def send(self) -> StickInitResponse | StickInitShortResponse | None: if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, (StickInitResponse, StickInitShortResponse)): + if isinstance(result, StickInitResponse | StickInitShortResponse): return result if result is None: return None From f276d1bb9882c622f68018cc3283034bdc05d9c9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:48:49 +0100 Subject: [PATCH 078/123] Add log-warning --- plugwise_usb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 3259d960b..e8d038098 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -276,6 +276,7 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: try: await self._controller.initialize_stick() except StickError as exc: + _LOGGER.warning("Cannot initialize Stick-connection: %s", exc) raise StickError(f"Cannot initialize Stick-connection: {exc}") from exc if self._network is None: From 9511d18a61c529df7b30162c6a60e50013377824 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:51:40 +0100 Subject: [PATCH 079/123] Bump to a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6c01be5a2..2de093fae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a3" +version = "0.48.0a4" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 53af8104545070eb6740b5954296f543ac0de7c0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:52:47 +0100 Subject: [PATCH 080/123] Remove is_connected requirement for mac_stick --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 77e8e1c06..e0d821aae 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -74,7 +74,7 @@ def hardware_stick(self) -> str | None: @property def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" - if not self._manager.is_connected or self._mac_stick is None: + if self._mac_stick is None: raise StickError( "No mac address available. Connect and initialize USB-Stick first." ) From e8e496189f375f623a871ae321b1cb2ad4f589a5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:53:14 +0100 Subject: [PATCH 081/123] Bump to a5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2de093fae..df5c134c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a4" +version = "0.48.0a5" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 28010f74304da535d30e80d52d8a8c18f7e8629c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 17:03:13 +0100 Subject: [PATCH 082/123] Disable now invalid test --- tests/test_usb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index b7668ac38..6b6399478 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -477,8 +477,8 @@ async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: unsub_connect() await stick.disconnect() assert not stick.network_state - with pytest.raises(pw_exceptions.StickError): - stick.mac_stick + # with pytest.raises(pw_exceptions.StickError): + # stick.mac_stick async def disconnected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined] """Handle disconnect event callback.""" From 3e972b3e9dc13fee52f07d48bc8c1871a6ddf5a0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:07 +0100 Subject: [PATCH 083/123] More debug-logging --- plugwise_usb/connection/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index e0d821aae..42ff88bbf 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -75,6 +75,7 @@ def hardware_stick(self) -> str | None: def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" if self._mac_stick is None: + _LOGGER.debug("mac_stick: %s", self._mac_stick) raise StickError( "No mac address available. Connect and initialize USB-Stick first." ) @@ -86,6 +87,8 @@ def mac_coordinator(self) -> str: Raises StickError when not connected. """ + _LOGGER.debug("mac_coordinator: %s", self._mac_nc) + _LOGGER.debug("is_connected: %s", self._manager.is_connected) if not self._manager.is_connected or self._mac_nc is None: raise StickError( "No mac address available. Connect and initialize USB-Stick first." From 9f9077b60ade91dc151abbd4234f4ea29ff4f884 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:26 +0100 Subject: [PATCH 084/123] Bump to a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df5c134c3..d8620c4c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a5" +version = "0.48.0a6" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From e103c498bf6cb270d39445cd066b12e83e089913 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:05:34 +0100 Subject: [PATCH 085/123] Move debug message --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 42ff88bbf..cbca49add 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -74,8 +74,8 @@ def hardware_stick(self) -> str | None: @property def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" + _LOGGER.debug("mac_stick: %s", self._mac_stick) if self._mac_stick is None: - _LOGGER.debug("mac_stick: %s", self._mac_stick) raise StickError( "No mac address available. Connect and initialize USB-Stick first." ) From 513449dcdc8800435231434de0b9a4175e70fa00 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:06:15 +0100 Subject: [PATCH 086/123] Bump to a7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d8620c4c2..4709efbc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a6" +version = "0.48.0a7" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 75f95d39f0f2500db46d7505e0a6dc40201c6e78 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:08:54 +0100 Subject: [PATCH 087/123] Revert adding try-except --- plugwise_usb/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index e8d038098..fa4de8144 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -273,12 +273,7 @@ async def plus_pair_request(self, mac: str) -> bool: @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" - try: - await self._controller.initialize_stick() - except StickError as exc: - _LOGGER.warning("Cannot initialize Stick-connection: %s", exc) - raise StickError(f"Cannot initialize Stick-connection: {exc}") from exc - + await self._controller.initialize_stick() if self._network is None: self._network = StickNetwork(self._controller) self._network.cache_folder = self._cache_folder From a20ea61546db5a909eea486c073884cf9bd74463 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:02:30 +0100 Subject: [PATCH 088/123] Replace debuggers by distinct message --- plugwise_usb/connection/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index cbca49add..0f848d430 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -74,10 +74,9 @@ def hardware_stick(self) -> str | None: @property def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" - _LOGGER.debug("mac_stick: %s", self._mac_stick) if self._mac_stick is None: raise StickError( - "No mac address available. Connect and initialize USB-Stick first." + "No mac_stick address available. Connect and initialize USB-Stick first." ) return self._mac_stick @@ -87,11 +86,9 @@ def mac_coordinator(self) -> str: Raises StickError when not connected. """ - _LOGGER.debug("mac_coordinator: %s", self._mac_nc) - _LOGGER.debug("is_connected: %s", self._manager.is_connected) if not self._manager.is_connected or self._mac_nc is None: raise StickError( - "No mac address available. Connect and initialize USB-Stick first." + "No mac_nc address available. Connect and initialize USB-Stick first." ) return self._mac_nc From 1a46e29e70c6c5e69bc2b2eafef297f4893bc931 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:04:26 +0100 Subject: [PATCH 089/123] Bump to a8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4709efbc7..b319fe595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a7" +version = "0.48.0a8" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 1c789573883faf30ea2209debb77f8f848c59d97 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:56:53 +0100 Subject: [PATCH 090/123] Remove unneeded StickError raises --- plugwise_usb/connection/__init__.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 0f848d430..af3f1fcdc 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -72,33 +72,18 @@ def hardware_stick(self) -> str | None: return self._hw_stick @property - def mac_stick(self) -> str: - """MAC address of USB-Stick. Raises StickError when not connected.""" - if self._mac_stick is None: - raise StickError( - "No mac_stick address available. Connect and initialize USB-Stick first." - ) + def mac_stick(self) -> str ? None: + """MAC address of USB-Stick.""" return self._mac_stick @property - def mac_coordinator(self) -> str: - """Return MAC address of the Zigbee network coordinator (Circle+). - - Raises StickError when not connected. - """ - if not self._manager.is_connected or self._mac_nc is None: - raise StickError( - "No mac_nc address available. Connect and initialize USB-Stick first." - ) + def mac_coordinator(self) -> str | None: + """Return MAC address of the Zigbee network coordinator (Circle+).""" return self._mac_nc @property - def network_id(self) -> int: - """Returns the Zigbee network ID. Raises StickError when not connected.""" - if not self._manager.is_connected or self._network_id is None: - raise StickError( - "No network ID available. Connect and initialize USB-Stick first." - ) + def network_id(self) -> int | None: + """Returns the Zigbee network ID.""" return self._network_id @property From 2706e3b810f2459e1726792922e8ad8d1ed54d86 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:01:58 +0100 Subject: [PATCH 091/123] Update relevant test-asserts --- tests/test_usb.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 6b6399478..5a0609f38 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -355,12 +355,9 @@ async def test_stick_connect_without_port(self) -> None: stick = pw_stick.Stick() assert stick.nodes == {} assert stick.joined_nodes is None - with pytest.raises(pw_exceptions.StickError): - stick.mac_stick - with pytest.raises(pw_exceptions.StickError): - stick.mac_coordinator - with pytest.raises(pw_exceptions.StickError): - stick.network_id + assert stick.mac_stick is None + assert stick.mac_coordinator is None + assert stick.network_id is None assert not stick.network_discovered assert not stick.network_state @@ -477,8 +474,6 @@ async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: unsub_connect() await stick.disconnect() assert not stick.network_state - # with pytest.raises(pw_exceptions.StickError): - # stick.mac_stick async def disconnected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined] """Handle disconnect event callback.""" From 96ce48a8c4151b8f99d26bc9e8777be1b77a79ad Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:05:41 +0100 Subject: [PATCH 092/123] Ruff fixes --- plugwise_usb/connection/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index af3f1fcdc..7ff75901d 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -72,7 +72,7 @@ def hardware_stick(self) -> str | None: return self._hw_stick @property - def mac_stick(self) -> str ? None: + def mac_stick(self) -> str | None: """MAC address of USB-Stick.""" return self._mac_stick @@ -186,7 +186,7 @@ async def initialize_stick(self) -> None: # Add Stick NodeInfoRequest node_info, _ = await self.get_node_details(self._mac_stick, ping_first=False) if node_info is not None: - self._fw_stick = node_info.firmware + self._fw_stick = node_info.firmware # type: ignore hardware, _ = version_to_model(node_info.hardware) self._hw_stick = hardware From 2af33e0bcb160e7be3b6a5e83e871f401c356717 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:24:55 +0100 Subject: [PATCH 093/123] Correct -update test_stick_network_down() --- tests/test_usb.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 5a0609f38..425c274dc 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1532,10 +1532,7 @@ async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None b"0011" # msg_id + b"0123456789012345" # stick mac + b"00" # unknown1 - + b"00" # network_is_online - + b"0098765432101234" # circle_plus_mac - + b"4321" # network_id - + b"00", # unknown2 + + b"00", # network_is_offline ), } ) @@ -1548,8 +1545,8 @@ async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect() - with pytest.raises(pw_exceptions.StickError): - await stick.initialize() + await stick.initialize() + assert stick.mac_coordinator is None await stick.disconnect() def fake_env(self, env: str) -> str | None: From 031bd12951e624d8146ef02d2e396809e211c40e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:29:50 +0100 Subject: [PATCH 094/123] Fix pylint warnings --- tests/test_usb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 425c274dc..8c922f0dc 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -194,9 +194,12 @@ async def mock_connection( loop: asyncio.AbstractEventLoop, protocol_factory: Callable[[], pw_receiver.StickReceiver], # type: ignore[name-defined] **kwargs: dict[str, Any], - ) -> tuple[DummyTransport, pw_receiver.StickReceiver]: # type: ignore[name-defined] + ) -> tuple[DummyTransport, pw_receiver.StickReceiver] | None: # type: ignore[name-defined] """Mock connection with dummy connection.""" self._protocol = protocol_factory() + if self._protocol is None: + return None + self._transport = DummyTransport(loop, self.custom_response) self._transport.protocol_data_received = self._protocol.data_received loop.call_soon_threadsafe(self._protocol.connection_made, self._transport) From bf27d384a5139dfe081dd1230a99eb177c03d61e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:46:58 +0100 Subject: [PATCH 095/123] More adapting to StickInitShortResponse --- plugwise_usb/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index fa4de8144..3ecfee62a 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -237,8 +237,8 @@ async def setup(self, discover: bool = True, load: bool = True) -> None: if not self.is_connected: await self.connect() if not self.is_initialized: - await self.initialize() - if discover: + initialized = await self.initialize() + if initialized and discover: await self.start_network() await self.discover_coordinator() await self.discover_nodes() @@ -271,9 +271,13 @@ async def plus_pair_request(self, mac: str) -> bool: return await self._controller.pair_plus_device(mac) @raise_not_connected - async def initialize(self, create_root_cache_folder: bool = False) -> None: + async def initialize(self, create_root_cache_folder: bool = False) -> bool: """Initialize connection to USB-Stick.""" await self._controller.initialize_stick() + # Check if network is offline = StickInitShortResponse + if self._controller.mac_coordinator is None: + return False + if self._network is None: self._network = StickNetwork(self._controller) self._network.cache_folder = self._cache_folder @@ -282,6 +286,8 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() + return True + @raise_not_connected @raise_not_initialized async def start_network(self) -> None: From abe49c4425fd6e226256048c3483759b62b83e39 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:12:14 +0100 Subject: [PATCH 096/123] Bump to a9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b319fe595..ab40344b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a8" +version = "0.48.0a9" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 0ee5ebe47fb15e8de4b408088544f82e791d1b4d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:40:51 +0100 Subject: [PATCH 097/123] Update Stick properties mac_stick, mac_coordinator and name --- plugwise_usb/__init__.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 3ecfee62a..476a354d8 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -130,18 +130,27 @@ def hardware(self) -> str: return self._controller.hardware_stick @property - def mac_stick(self) -> str: - """MAC address of USB-Stick. Raises StickError is connection is missing.""" + def mac_stick(self) -> str | None: + """MAC address of USB-Stick. + + Returns None when the connection to the Stick fails. + """ return self._controller.mac_stick @property - def mac_coordinator(self) -> str: - """MAC address of the network coordinator (Circle+). Raises StickError is connection is missing.""" + def mac_coordinator(self) -> str | None: + """MAC address of the network coordinator (Circle+). + + Returns none when there is no connection, not paired, not present in the network. + """ return self._controller.mac_coordinator @property - def name(self) -> str: - """Return name of Stick.""" + def name(self) -> str | None: + """Return name of Stick. + + Returns None when the connection to the Stick fails. + """ return self._controller.stick_name @property From 6fd640ed1e2803da1146a7f552983ef9d2a16d2d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:51:53 +0100 Subject: [PATCH 098/123] Update docstring --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 7ff75901d..3069a9816 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -246,7 +246,7 @@ async def pair_plus_device(self, mac: str) -> bool: async def get_node_details( self, mac: str, ping_first: bool ) -> tuple[NodeInfoResponse | None, NodePingResponse | None]: - """Return node discovery type.""" + """Collect NodeInfo data from the Stick.""" ping_response: NodePingResponse | None = None if ping_first: # Define ping request with one retry From 25c1bc7f41fe1a83b3938385cdcd076b3cf58ff3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 12:20:46 +0100 Subject: [PATCH 099/123] Update network_online docstring --- plugwise_usb/connection/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 3069a9816..4fbb940cb 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -88,7 +88,11 @@ def network_id(self) -> int | None: @property def network_online(self) -> bool: - """Return the network state.""" + """Return the network state. + + The ZigBee network is online when the Stick is connected and a + StickInitResponse indicates that the ZigBee network is online. + """ if not self._manager.is_connected: raise StickError( "Network status not available. Connect and initialize USB-Stick first." From 65867a07b1293555d752f66221a9237631adc787 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 14:21:56 +0100 Subject: [PATCH 100/123] Bump to a10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ab40344b7..c271daf43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a9" +version = "0.48.0a10" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 59d5c6d51f01473ecf98e8c1b182de887236db43 Mon Sep 17 00:00:00 2001 From: autoruff Date: Fri, 20 Feb 2026 13:22:59 +0000 Subject: [PATCH 101/123] fixup: pair-plus Python code fixed using Ruff --- plugwise_usb/__init__.py | 6 +++--- plugwise_usb/connection/__init__.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 476a354d8..7bcc546fb 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -132,7 +132,7 @@ def hardware(self) -> str: @property def mac_stick(self) -> str | None: """MAC address of USB-Stick. - + Returns None when the connection to the Stick fails. """ return self._controller.mac_stick @@ -140,7 +140,7 @@ def mac_stick(self) -> str | None: @property def mac_coordinator(self) -> str | None: """MAC address of the network coordinator (Circle+). - + Returns none when there is no connection, not paired, not present in the network. """ return self._controller.mac_coordinator @@ -148,7 +148,7 @@ def mac_coordinator(self) -> str | None: @property def name(self) -> str | None: """Return name of Stick. - + Returns None when the connection to the Stick fails. """ return self._controller.stick_name diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 4fbb940cb..a935d2765 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -90,7 +90,7 @@ def network_id(self) -> int | None: def network_online(self) -> bool: """Return the network state. - The ZigBee network is online when the Stick is connected and a + The ZigBee network is online when the Stick is connected and a StickInitResponse indicates that the ZigBee network is online. """ if not self._manager.is_connected: @@ -279,7 +279,7 @@ async def send( return await self._queue.submit(request) try: return await self._queue.submit(request) - except (NodeError, StickError): + except NodeError, StickError: return None def _reset_states(self) -> None: From a257129776fe0748a7544e01eca69513ba9d150f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 09:46:34 +0100 Subject: [PATCH 102/123] Responses: line up Int() use --- plugwise_usb/messages/responses.py | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 9f620216e..30acd7c6b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -332,7 +332,7 @@ class NodeSpecificResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeSpecificResponse message object.""" super().__init__(b"0003") - self.status = Int(0, 4) + self.status = Int(0, length=4) self._params += [self.status] @@ -346,8 +346,8 @@ class CirclePlusConnectResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize CirclePlusConnectResponse message object.""" super().__init__(b"0005") - self.existing = Int(0, 2) - self.allowed = Int(0, 2) + self.existing = Int(0, length=2) + self.allowed = Int(0, length=2) self._params += [self.existing, self.allowed] @@ -500,13 +500,13 @@ class CirclePowerUsageResponse(PlugwiseResponse): def __init__(self, protocol_version: str = "2.3") -> None: """Initialize CirclePowerUsageResponse message object.""" super().__init__(b"0013") - self._pulse_1s = Int(0, 4) - self._pulse_8s = Int(0, 4) - self._nanosecond_offset = Int(0, 4) + self._pulse_1s = Int(0, length=4) + self._pulse_8s = Int(0, length=4) + self._nanosecond_offset = Int(0, length=4) self._params += [self._pulse_1s, self._pulse_8s] if protocol_version == "2.3": - self._pulse_counter_consumed = Int(0, 8) - self._pulse_counter_produced = Int(0, 8) + self._pulse_counter_consumed = Int(0, length=8) + self._pulse_counter_produced = Int(0, length=8) self._params += [ self._pulse_counter_consumed, self._pulse_counter_produced, @@ -608,7 +608,7 @@ def __init__(self) -> None: """Initialize NodeRemoveResponse message object.""" super().__init__(b"001D") self.node_mac_id = String(None, length=16) - self.status = Int(0, 2) + self.status = Int(0, length=2) self._params += [self.node_mac_id, self.status] @@ -757,8 +757,8 @@ def __init__(self) -> None: super().__init__(b"003F") self.time = Time() self.day_of_week = Int(0, 2, False) - self.unknown = Int(0, 2) - self.unknown2 = Int(0, 4) + self.unknown = Int(0, length=2) + self.unknown2 = Int(0, length=4) self._params += [ self.time, self.day_of_week, @@ -779,13 +779,13 @@ def __init__(self) -> None: """Initialize CircleEnergyLogsResponse message object.""" super().__init__(b"0049") self.logdate1 = DateTime() - self.pulses1 = Int(0, 8) + self.pulses1 = Int(0, length=8) self.logdate2 = DateTime() - self.pulses2 = Int(0, 8) + self.pulses2 = Int(0, length=8) self.logdate3 = DateTime() - self.pulses3 = Int(0, 8) + self.pulses3 = Int(0, length=8) self.logdate4 = DateTime() - self.pulses4 = Int(0, 8) + self.pulses4 = Int(0, length=8) self._logaddr = LogAddr(0, length=8) self._params += [ self.logdate1, @@ -969,7 +969,7 @@ class SenseReportResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize SenseReportResponse message object.""" super().__init__(SENSE_REPORT_ID) - self.humidity = Int(0, length=4, negative=False) + self.humidity = Int(0, 4, False) self.temperature = Int(0, length=4) self._params += [self.humidity, self.temperature] From f63fdb9795be013ad0b6625fa21372ca4cf69827 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 09:59:36 +0100 Subject: [PATCH 103/123] Add missing decode_mac=False --- plugwise_usb/messages/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 30acd7c6b..7d1fa6481 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -288,7 +288,7 @@ class StickNetworkInfoResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeNetworkInfoResponse message object.""" - super().__init__(b"0002") + super().__init__(b"0002", decode_mac=False) self._channel = Int(0, length=2) self._source_mac_id = String(None, length=16) self.extended_pan_id = String(None, length=16) @@ -331,7 +331,7 @@ class NodeSpecificResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeSpecificResponse message object.""" - super().__init__(b"0003") + super().__init__(b"0003", decode_mac=False) self.status = Int(0, length=4) self._params += [self.status] From b7f41f44db6468ec5c9bcb5d572ae033cab76f60 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 10:26:34 +0100 Subject: [PATCH 104/123] Correct 0002-format in response --- tests/test_pairing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index bf1037497..27da7b99f 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -47,7 +47,6 @@ "Stick network info request", b"000000C1", # Success ack b"0002" # response msg_id - + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" + b"FF98765432101234" # 06 + plus-device mac From 27df76d366616ea62c41ef96f059f32c6f6dcf1e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 10:47:55 +0100 Subject: [PATCH 105/123] Bump to a11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c271daf43..0f9e47749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a10" +version = "0.48.0a11" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 4b12d22ed8143f9f34717be119570d0e2bfb03ce Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 11:18:38 +0100 Subject: [PATCH 106/123] Exit when network is not online --- plugwise_usb/connection/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index a935d2765..ab9467004 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,7 +220,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None - _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -229,7 +228,8 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - _LOGGER.debug("HOI Init done") + if not self._network_online: + raise NodeError("Pairing failed, network is not online") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,8 +243,6 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") - _LOGGER.debug("HOI PlusConnectRequest done") - return True async def get_node_details( From 2403662156d3f8721dbc12f5569ae6140cfc35e9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 11:24:56 +0100 Subject: [PATCH 107/123] Bump to a12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f9e47749..f7774c9af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a11" +version = "0.48.0a12" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 8cd30c6b0a04d9128fc5a65b3503610dff67bfbf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:07:07 +0100 Subject: [PATCH 108/123] Revert "Exit when network is not online" This reverts commit 9d5ba855f5ba5c2dbed599b452f00382ee62157e. --- plugwise_usb/connection/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index ab9467004..a935d2765 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,6 +220,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None + _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -228,8 +229,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - if not self._network_online: - raise NodeError("Pairing failed, network is not online") + _LOGGER.debug("HOI Init done") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,6 +243,8 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") + _LOGGER.debug("HOI PlusConnectRequest done") + return True async def get_node_details( From 50c78e90c0b5eaf976361d949d11ac67aae2c94a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:08:38 +0100 Subject: [PATCH 109/123] Remove logger-HOI-lines --- plugwise_usb/connection/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index a935d2765..2f89f1059 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,7 +220,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None - _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -229,7 +228,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - _LOGGER.debug("HOI Init done") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,8 +241,6 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") - _LOGGER.debug("HOI PlusConnectRequest done") - return True async def get_node_details( From 94cd7e590312cb3a8ca8f5bf7ab7ce28a8f38879 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:18:43 +0100 Subject: [PATCH 110/123] Bump to a13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f7774c9af..5e9fdb3db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a12" +version = "0.48.0a13" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 048058ea5666644b20aadb56b555ec0c4c0e343a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 15:28:44 +0100 Subject: [PATCH 111/123] Don't collect NodeInfo during pairing --- plugwise_usb/connection/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 2f89f1059..640eec4ce 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -151,7 +151,7 @@ async def _handle_stick_event(self, event: StickEvent) -> None: elif event == StickEvent.DISCONNECTED and self._queue.is_running: await self._queue.stop() - async def initialize_stick(self) -> None: + async def initialize_stick(self, node_info=True) -> None: """Initialize connection to the USB-stick.""" if not self._manager.is_connected: raise StickError( @@ -187,7 +187,10 @@ async def initialize_stick(self) -> None: self._is_initialized = True - # Add Stick NodeInfoRequest + if not node_info: + return + + # Collect Stick NodeInfo node_info, _ = await self.get_node_details(self._mac_stick, ping_first=False) if node_info is not None: self._fw_stick = node_info.firmware # type: ignore @@ -223,7 +226,7 @@ async def pair_plus_device(self, mac: str) -> bool: # Init Stick try: - await self.initialize_stick() + await self.initialize_stick(node_info=False) except StickError as exc: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" From a3c2c593dcb5d2f9a9fa108b2e757b607303ee5b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 16:23:21 +0100 Subject: [PATCH 112/123] Update 0004-request --- plugwise_usb/messages/requests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 4b35a865d..9c338b836 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,8 +406,9 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"00000000000000000000" - msg: bytes = self._identifier + args + special_id = b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" + args = b"0000000000000000" + msg: bytes = self._identifier + special_id + args if self._mac is not None: msg += self._mac checksum = self.calculate_checksum(msg) From c454a3c3790be228ed5314c7d91894563494a174 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 16:36:04 +0100 Subject: [PATCH 113/123] Update test-related --- tests/test_pairing.py | 2 +- tests/test_usb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 27da7b99f..a6c33adec 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -66,7 +66,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x0300040000000000000000000000987654321012344D73\r\n": ( + b"\x05\x05\x03\x030004000100000000000000000098765432101234C0AF\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id diff --git a/tests/test_usb.py b/tests/test_usb.py index 8c922f0dc..7320a7ea0 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1447,7 +1447,7 @@ async def test_creating_request_messages(self) -> None: ) assert ( circle_plus_connect_request.serialize() - == b"\x05\x05\x03\x030004000000000000000000001111222233334444BDEC\r\n" + == b"\x05\x05\x03\x0300040001000000000000000011112222333344443030\r\n" ) node_add_request = pw_requests.NodeAddRequest( self.dummy_fn, b"1111222233334444", True From b631ebd106289c0926ad08758ddbb10721440ee2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 18:59:09 +0100 Subject: [PATCH 114/123] Ruffed --- plugwise_usb/messages/requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 9c338b836..d77ba7976 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,9 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - special_id = b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" + special_id = ( + b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" + ) args = b"0000000000000000" msg: bytes = self._identifier + special_id + args if self._mac is not None: From 6a5ec29d3dc1d1db7b88e686843524d06fec6a49 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 19:06:30 +0100 Subject: [PATCH 115/123] Update docstring --- plugwise_usb/messages/requests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index d77ba7976..e73f1adbb 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -400,15 +400,15 @@ async def send(self) -> CirclePlusConnectResponse | None: # This message has an exceptional format and therefore # need to override the serialize method def serialize(self) -> bytes: - """Convert message to serialized list of bytes.""" - # This command has - # args: byte - # key, byte - # network info.index, ulong - # network key = 0 - special_id = ( - b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" - ) + """Convert message to serialized list of bytes. + + Parameters + ---------- + - special_id: byte - observed sequence with retry: b"0001", B"0101", + - and args: byte. + + """ + special_id = b"0001" args = b"0000000000000000" msg: bytes = self._identifier + special_id + args if self._mac is not None: From aec5d3c5c7051e94625a634bab76af08008c7417 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 08:59:41 +0100 Subject: [PATCH 116/123] Bump to a14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5e9fdb3db..46390ddbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a13" +version = "0.48.0a14" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 7ed5066c3ff34f17afe42120aa3660f4e00749e9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 09:29:53 +0100 Subject: [PATCH 117/123] 0005-response: add missing decode_mac=False --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 7d1fa6481..998871d8b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -345,7 +345,7 @@ class CirclePlusConnectResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize CirclePlusConnectResponse message object.""" - super().__init__(b"0005") + super().__init__(b"0005", decode_mac=False) self.existing = Int(0, length=2) self.allowed = Int(0, length=2) self._params += [self.existing, self.allowed] From 6c883856c8fa2ca9d96defe32a2c8c2549665d35 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 09:47:33 +0100 Subject: [PATCH 118/123] Improve docstring --- plugwise_usb/connection/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 640eec4ce..59ceab20e 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -202,12 +202,16 @@ async def pair_plus_device(self, mac: str) -> bool: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), - 000A - 0011: StickInitRequest - StickInitResponse, + 0001 - 0002 - 0003: StickNetworkInfoRequest - StickNetworkInfoResponse - NodeSpecificResponse, + 000A - 0011: StickInitRequest - StickInitShortResponse/StickInitResponse, 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, the Plus-device will then send a NodeRejoinResponse (0061). - Todo(?): Does this need repeating until pairing is successful? + In the first occurrence of this process a 0004 0001 .... message is sent. + A StickInitShortResponse is received indicating the network is offline. + In the second occurrence of this process a 0004 0101 .... message is sent. + Again a StickInitShortResponse is received. + In the third occurrence only 000A is sent and a StickInitResponse indicating the network is online, is received. """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From fbd75dbd9f062f1bd020efc08f6035362e61682e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Mar 2026 18:28:32 +0100 Subject: [PATCH 119/123] Adapt 0005-test-response --- tests/test_pairing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index a6c33adec..f62398c7d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -70,7 +70,6 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id - + b"0098765432101234" # circle+ mac + b"00" # existing + b"01", # allowed ), From 0d16ba2e76b58eb796c3fc7cc1ad3faacec3cfdc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 3 Mar 2026 08:23:51 +0100 Subject: [PATCH 120/123] Add init to StickNetworkInfoRequest --- plugwise_usb/messages/requests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index e73f1adbb..ec14e83e3 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -364,8 +364,18 @@ class StickNetworkInfoRequest(PlugwiseRequest): _identifier = b"0001" _reply_identifier = b"0002" + def __init__( + self, + send_fn: Callable[[PlugwiseRequest, bool], Awaitable[PlugwiseResponse | None]], + ) -> None: + """Initialize StickInitRequest message object.""" + super().__init__(send_fn, None) + self._max_retries = 1 + async def send(self) -> StickNetworkInfoResponse | None: """Send request.""" + if self._send_fn is None: + raise MessageError("Send function missing") result = await self._send_request() if isinstance(result, StickNetworkInfoResponse): return result From c4db1f880cb14a5aeadb08b7d75599dac2a7709d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 3 Mar 2026 08:24:48 +0100 Subject: [PATCH 121/123] And adapt use --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 59ceab20e..aaa8f5748 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -219,7 +219,7 @@ async def pair_plus_device(self, mac: str) -> bool: # Collect network info try: - request = StickNetworkInfoRequest(self.send, None) + request = StickNetworkInfoRequest(self.send) info_response = await request.send() except MessageError as exc: raise NodeError(f"Pairing failed: {exc}") from exc From aa846d366df7a078a6f1700aefd2d66a2f4cdc57 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Mar 2026 20:02:48 +0100 Subject: [PATCH 122/123] Disable guarding that breaks 0004-0005 sequence detection --- plugwise_usb/connection/receiver.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/connection/receiver.py b/plugwise_usb/connection/receiver.py index 1dee3f8a4..6cf9103da 100644 --- a/plugwise_usb/connection/receiver.py +++ b/plugwise_usb/connection/receiver.py @@ -441,11 +441,11 @@ async def _notify_node_response_subscribers( notify_tasks: list[Coroutine[Any, Any, bool]] = [] for node_subscription in self._node_response_subscribers.values(): - if ( - node_subscription.mac is not None - and node_subscription.mac != node_response.mac - ): - continue + # if ( + # node_subscription.mac is not None + # and node_subscription.mac != node_response.mac + # ): + # continue if ( node_subscription.response_ids is not None and node_response.identifier not in node_subscription.response_ids From 689dd89c24e6ea878aecbf8bf6f9962c8ad42d2d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Mar 2026 20:11:51 +0100 Subject: [PATCH 123/123] Fix CHANGELOG after rebase --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464190c81..b76d9b4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Ongoing + +- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) + ## v0.47.8 - 2026-07-01 PR [459](https://github.com/plugwise/python-plugwise-usb/pull/459): Reduce logging for SEDs @@ -22,8 +26,8 @@ PR [422](https://github.com/plugwise/python-plugwise-usb/pull/422): Add missing ## v0.47.3 - 2026-03-04 +- PR [418](https://github.com/plugwise/python-plugwise-usb/pull/418): Improve raise-message for better debugging - PR [409](https://github.com/plugwise/python-plugwise-usb/pull/409): Fix recent Ruff errors -- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) ## v0.47.2 - 2026-01-29