From 80e2e57ee406aa75172089b0d95398d3afb69903 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Feb 2026 10:47:36 +0100 Subject: [PATCH 001/103] 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 0d76cf06738466a97b6d93e6fdb553ff0c1d107a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:19:52 +0100 Subject: [PATCH 002/103] 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 91a070eb9d9c38d212b19f2a4f5ec539b616255c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:43:00 +0100 Subject: [PATCH 003/103] 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 7cf24838430edf7721b63bd5e67e3962b0d35010 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:49:10 +0100 Subject: [PATCH 004/103] 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 b50e2ad729588384e7880d1493dc9d4924a62511 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 18:42:54 +0100 Subject: [PATCH 005/103] 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 3f439e5693ab3c6fb9d11b28f1edfd31a1a42574 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 19:29:26 +0100 Subject: [PATCH 006/103] 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 ed5480e7f7b39aca81de3c7ddce31c990526c462 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:11:42 +0100 Subject: [PATCH 007/103] 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 c9fe588f447f35886bf5c3a5672d92f5d53fdf60 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:23:34 +0100 Subject: [PATCH 008/103] 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 29ebc4ae462989344be74f785c132a6aae7a95fa Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:28:33 +0100 Subject: [PATCH 009/103] 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 077e9c2714865f2d82072122f9a77719eb79feee Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:31:31 +0100 Subject: [PATCH 010/103] 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 093db61009549819b07a386d5d9421e6f7b50430 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/103] 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 a8223cf415ac7c855c0dd8e7af46e58752567b8d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:06:35 +0100 Subject: [PATCH 012/103] 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 a0a7e3251e2798112b05a36b837bba88748876e4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:10:35 +0100 Subject: [PATCH 013/103] 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 a637c9d997444adba7d25af9a079f5e6f0a93141 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:19:39 +0100 Subject: [PATCH 014/103] 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 75a63f128d3b010ee808f943a142e36dbe5c8e34 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:32:20 +0100 Subject: [PATCH 015/103] 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 bc0a38464fd25994b7238f410b93ccd259feef1c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:40:48 +0100 Subject: [PATCH 016/103] 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 9e85edda1c74360cf8f159ad25d0ab7a2e2118f8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:18:14 +0100 Subject: [PATCH 017/103] 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 1fe39ba5fa96a8ec9a77d8c03277cc9cb465a8bd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:51:31 +0100 Subject: [PATCH 018/103] 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 18d8db45cf10c8b585f162ab8bca5417e096b235 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:58:46 +0100 Subject: [PATCH 019/103] 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 6a19332aa2b4a391a0eee960ffbde98be3d4b8ab Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:09:13 +0100 Subject: [PATCH 020/103] 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 bed2a00558bf1c9c8e7e4773d665051287e39d74 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:10:41 +0100 Subject: [PATCH 021/103] 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 0bbe0762d0d0d38fa3cd21706d47652e2925f80d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:12:44 +0100 Subject: [PATCH 022/103] 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 ff377a5fc00004d3cca20d12e05ddb7412968016 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:26:33 +0100 Subject: [PATCH 023/103] 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 74da5ead8..30aca34b9 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -426,16 +426,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 0dc072a1ce460ef4703bd1312531a9de719e6000 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:35:33 +0100 Subject: [PATCH 024/103] 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 30aca34b9..e3a823b5f 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -430,17 +430,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 2b37197739f32157a07e872891aae9a27ce497e0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:41:24 +0100 Subject: [PATCH 025/103] 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 e3a823b5f..321e471ed 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -429,7 +429,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) @@ -443,14 +445,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 d5ca1470ed9195753c0ae933100c75e697e66d9c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:16:16 +0100 Subject: [PATCH 026/103] 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 321e471ed..542a3233e 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -409,8 +409,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 @@ -426,37 +463,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 @@ -1003,8 +1029,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 a698db06304e49fff9dac38f77d3968067e584e8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:28:26 +0100 Subject: [PATCH 027/103] 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 3be0172d7da2e12f4d2cb494cf4984b3aa6b17d5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:34:02 +0100 Subject: [PATCH 028/103] 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 542a3233e..b9b9f4bd1 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1032,9 +1032,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 a014b795cc69c9dbf3a4bf899ddccd4fc5568b7c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:41:44 +0100 Subject: [PATCH 029/103] 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 3184b875b..906bdb978 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -38,6 +38,7 @@ NodeSpecificResponse, PlugwiseResponse, StickInitResponse, + StickInitShortResponse, StickNetworkInfoResponse, StickResponse, StickResponseType, @@ -514,7 +515,7 @@ class StickInitRequest(PlugwiseRequest): """Initialize USB-Stick. Supported protocols : 1.0, 2.0 - Response message : StickInitResponse + Response message : StickInitResponse or StickInitShortResponse """ _identifier = b"000A" @@ -528,17 +529,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 531bea210704cc32952500aa158127b8d4ccf79d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:49:23 +0100 Subject: [PATCH 030/103] 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 b9b9f4bd1..ca713bc43 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1034,7 +1034,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 191d138b55c6bb621d521416b76df48f8badd95c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:57:19 +0100 Subject: [PATCH 031/103] 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 f876bbe100bd45c0a7abc159dd3a5dcdbb40caf1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:59:30 +0100 Subject: [PATCH 032/103] 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 749dec0f5..8b3c6a38f 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -57,7 +57,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 d22bf3806a031c3df9df740d8a8644a9f36bebb2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:11:30 +0100 Subject: [PATCH 033/103] 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 305bebcb9d18faa411a1a30cb134d916c6eba205 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:13:36 +0100 Subject: [PATCH 034/103] 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 8d970af47d3483ed270ca8f9472ef368bb840fad Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:42:31 +0100 Subject: [PATCH 035/103] 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 d5249480e861c1b9f87065b0379444156d8e4ad6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:46:12 +0100 Subject: [PATCH 036/103] 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 9d4b8e593669d1b70af8e56b1cc97999e749f6a8 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sun, 8 Feb 2026 12:49:30 +0000 Subject: [PATCH 037/103] 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 906bdb978..6c5ac6de2 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -534,7 +534,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 0a95d82f47793e52808639832fbe93d0d8ea83b2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:33:41 +0100 Subject: [PATCH 038/103] 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 347f7b90b412e4377853a40aec07b1f809f1a445 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:38:21 +0100 Subject: [PATCH 039/103] 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 ca713bc43..a7f4e489d 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -446,7 +446,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: @@ -460,15 +460,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, @@ -485,15 +481,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 2bcc2cdf2bec3caf1cfc0e47bfb094b4f3b30de8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:52:34 +0100 Subject: [PATCH 040/103] 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 2ed21cfb4..5e168a4a9 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 4415e63aff7979969e30c12bf16d08063dd4c7ae Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:03:58 +0100 Subject: [PATCH 041/103] 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 b4c446fbf66920f5821929f44d514e44371f3d6e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:08:47 +0100 Subject: [PATCH 042/103] 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 78eb3033549cd3abf042bc0bdfc1cbd2cace7a37 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:35:55 +0100 Subject: [PATCH 043/103] 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 d3528d6749ecfc273b9d8f8c1cfc31e0896bbb0f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:46:23 +0100 Subject: [PATCH 044/103] 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 f16b4b2f1a58d482510273a2571fa804d6a93be4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:47:25 +0100 Subject: [PATCH 045/103] 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 5e168a4a9..fdc129fe6 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 3e6f9ed1a3e1ac1e2111c15dbcc795abc7b9e3cc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:49:34 +0100 Subject: [PATCH 046/103] 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 907c63b191647c29b21402eb63c4d204185c81ad Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:52:43 +0100 Subject: [PATCH 047/103] 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 c32bdf4827fe5c45a0a326b18e118fe5e6f6ab42 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 9 Feb 2026 07:56:13 +0000 Subject: [PATCH 048/103] 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 4830494bddf31c790cbff8b55c5834187bf5ac20 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:08:32 +0100 Subject: [PATCH 049/103] 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 fdc129fe6..eef09d048 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 0d774a3a6cb6e6cd05e705e77fd76dea9cd14657 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:15:32 +0100 Subject: [PATCH 050/103] 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 eef09d048..c1b883a29 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 5c25fe144694cd8fa67885ea4285339cc29142fb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:27:31 +0100 Subject: [PATCH 051/103] 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 5269be23ad0b74aa3152d84058ec52052d4d97a0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:28:46 +0100 Subject: [PATCH 052/103] 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 5afef3a80a24f9724ffd860a4ecbb5aa684f7305 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:32:54 +0100 Subject: [PATCH 053/103] 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 6c5ac6de2..1222e5f64 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -397,7 +397,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 4062dd64de85688456f2bd64fa2ef78536851621 Mon Sep 17 00:00:00 2001 From: autoruff Date: Tue, 10 Feb 2026 07:34:30 +0000 Subject: [PATCH 054/103] 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 01b0a63378f45ad45560ffee6107cbe9e42760e8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:04:26 +0100 Subject: [PATCH 055/103] 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 fe7e64ab2aaa0fed46b5591ab80c79c1004ebce2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:10:31 +0100 Subject: [PATCH 056/103] 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 1d9484291ab97dfa9e610c1da7a4d2a708ed80e6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:31:16 +0100 Subject: [PATCH 057/103] 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 1222e5f64..5409675ad 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -397,7 +397,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 d10e7b473fde59a85d6e61e1eaf07854d8600084 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:47:09 +0100 Subject: [PATCH 058/103] 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 d91928b15228f55757c2b6ab68edd18b2e1e053b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 12 Feb 2026 19:55:47 +0100 Subject: [PATCH 059/103] 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 5409675ad..6c5ac6de2 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -397,7 +397,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 1da5ab9fb025c98e98dfc6493f743b64367e714a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 07:59:09 +0100 Subject: [PATCH 060/103] 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 31c8e624851c5153fb3a38be754d811b56aaf487 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:03:48 +0100 Subject: [PATCH 061/103] 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 fbb9495a9d881c5581eba3da2f0a71a2dd2bc435 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:05:23 +0100 Subject: [PATCH 062/103] 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 1e8ef50f5f089a84089d784f4c8fb4eafb762eaa Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:08:49 +0100 Subject: [PATCH 063/103] 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 78f5ec720f02a1e67a5c55ee7588b2620c7319e7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:35:20 +0100 Subject: [PATCH 064/103] 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 527b73ea2b19190c8b50dbd0c364ba2fa7a7c0d1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:56:13 +0100 Subject: [PATCH 065/103] 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 d0befabeadb88631f9af9f220635f8661f7b5033 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:15:14 +0100 Subject: [PATCH 066/103] 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 a674759e6..5edce951b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.47.2" +version = "0.48.0a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 6e958d95c28df8310afcb4ade9d9483ef071270b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:17:07 +0100 Subject: [PATCH 067/103] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f3dc337..a91be701a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - 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 987d30c5da1ed44f2d5a4d784d7d34167d5dae2e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:41:52 +0100 Subject: [PATCH 068/103] 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 c1b883a29..cfb8b22c6 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 35c166be22bb8a50bb598a90e920996e3b1df2ac Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:44:42 +0100 Subject: [PATCH 069/103] 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 8b3c6a38f..40347d122 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -57,8 +57,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 59d9e4282562cdc868e1dc4befe18f4c0d4c7fda Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:22:04 +0100 Subject: [PATCH 070/103] 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 6c5ac6de2..a795bef27 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -534,9 +534,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 f41bb813e3db99e2c117f920455b770c27daed24 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:28:28 +0100 Subject: [PATCH 071/103] Correct CHANGELOG after rebase --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91be701a..ab9ff5b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## Ongoing updates -- 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 cca233eaf16084d9b89bd672517e92798fe119f6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:29:04 +0100 Subject: [PATCH 072/103] 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 5edce951b..5fc3e9933 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 f8e6635a78b9260b45cc8f5817379220afd7a361 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:31:46 +0100 Subject: [PATCH 073/103] 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 40347d122..749dec0f5 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -57,7 +57,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 64eb055ce110db7517523d64012a822e86ffdda8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 10:45:38 +0100 Subject: [PATCH 074/103] Revert back to python 3.13 --- .github/workflows/merge.yml | 2 +- .github/workflows/verify.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 23a547c14..e17592edd 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -5,7 +5,7 @@ name: Latest release env: CACHE_VERSION: 22 - DEFAULT_PYTHON: "3.14" + DEFAULT_PYTHON: "3.13" # Only run on merges on: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 96c6336ad..9d9281811 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -5,7 +5,7 @@ name: Latest commit env: CACHE_VERSION: 1 - DEFAULT_PYTHON: "3.14" + DEFAULT_PYTHON: "3.13" PRE_COMMIT_HOME: ~/.cache/pre-commit VENV: venv @@ -133,7 +133,7 @@ jobs: - commitcheck strategy: matrix: - python-version: ["3.14"] + python-version: ["3.13"] steps: - name: Check out committed code uses: actions/checkout@v6 From aabcc304f325fb34e276973b5884991f21780de8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 10:46:35 +0100 Subject: [PATCH 075/103] Refix except brackets --- 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 cfb8b22c6..77e8e1c06 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -290,7 +290,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 f72a4ac288ac853163adac1000ed15d340e2fc51 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 11:03:36 +0100 Subject: [PATCH 076/103] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5fc3e9933..675970026 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 70d06049a67c55af623cbeb107e6ad5bfb56d5af Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:33:49 +0100 Subject: [PATCH 077/103] 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 2e674149694f600dd1bf2aa0eaa149b9f3c53f8f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:46:16 +0100 Subject: [PATCH 078/103] 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 a795bef27..b4f40247e 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -534,7 +534,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 03b3d9b18f78887e72c014bbd2fecbe8ba89dcbd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:48:49 +0100 Subject: [PATCH 079/103] 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 a691dd66cb4355ce898656270f29a116b58f9de8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:51:40 +0100 Subject: [PATCH 080/103] Bump to a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 675970026..bf57859cb 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 2a6abcb963b08a2d653c0f1ade9b69317ca09cfc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:52:47 +0100 Subject: [PATCH 081/103] 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 08640051bbe90cef4c1408a0d83f7a1da8ded532 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:53:14 +0100 Subject: [PATCH 082/103] Bump to a5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bf57859cb..cf0be51bb 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 b34a20be55f1ba8ebf4e98bbae13702e4229fb6c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 17:03:13 +0100 Subject: [PATCH 083/103] 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 5350745da..9281212f8 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 a4daff030e2b5e9ad25e0a49f426270a9d4f3552 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:07 +0100 Subject: [PATCH 084/103] 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 afd2d11ab2fc2455a676ceda0b63a6c01c925f48 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:26 +0100 Subject: [PATCH 085/103] Bump to a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf0be51bb..114760a9a 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 e80c7839d799cafeea2af4b02c14db171bdd0e41 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:05:34 +0100 Subject: [PATCH 086/103] 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 65ab3c206294fe184b76666440e042cf04ab97fe Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:06:15 +0100 Subject: [PATCH 087/103] Bump to a7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 114760a9a..eb77a0983 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 e2708934eb3a6311748bf4cf5bb01eae8db8a9e2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:08:54 +0100 Subject: [PATCH 088/103] 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 58ee1c2575d1dbaaf76cb51cab508d7c2fd68337 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:02:30 +0100 Subject: [PATCH 089/103] 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 ca9df05911723cc3d27b676216400cd712d0db0b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:04:26 +0100 Subject: [PATCH 090/103] Bump to a8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb77a0983..45611748f 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 3f4a8e0df4ded3efa7bdbb2f039aaa40a7751ef2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:56:53 +0100 Subject: [PATCH 091/103] 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 b5435150b3f87d283da756a9f440bd8a3a35c623 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:01:58 +0100 Subject: [PATCH 092/103] 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 9281212f8..534c1a0be 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 3c98822dc63e0344c52cf6543da8cd7507137b69 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:05:41 +0100 Subject: [PATCH 093/103] 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 c6165441398ce723605bea5844c13812d0083119 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:24:55 +0100 Subject: [PATCH 094/103] 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 534c1a0be..830f9797d 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 45e1d2d3adea5793315883b707cc5ef5579f15ff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:29:50 +0100 Subject: [PATCH 095/103] 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 830f9797d..9a883f8d1 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 f1276adc74c370820953b8f93eb2ddda4cc52730 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:46:58 +0100 Subject: [PATCH 096/103] 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 c62c703729768a7b8af0cf169f876016b66808c6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:12:14 +0100 Subject: [PATCH 097/103] Bump to a9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 45611748f..9b2ff48ef 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 4f9299309d68f2563c0852ffd4f882f7a6dc47f9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:40:51 +0100 Subject: [PATCH 098/103] 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 37a0930bed15c2deec475ad22472a722c17f52ee Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:51:53 +0100 Subject: [PATCH 099/103] 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 593d06299820bffe4feaa358dc3e56eaf34a8c6f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 12:20:46 +0100 Subject: [PATCH 100/103] 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 7b2391eb4aa3ffd27c32edee01fa00861668210e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 14:21:56 +0100 Subject: [PATCH 101/103] Bump to a10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9b2ff48ef..813ef42a4 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 cacef482114af3aaa819697dc5ac0b07f862e5f5 Mon Sep 17 00:00:00 2001 From: autoruff Date: Fri, 20 Feb 2026 13:22:59 +0000 Subject: [PATCH 102/103] 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 02b355597ff440cb1e91410547a83cda6407e600 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 14:33:45 +0100 Subject: [PATCH 103/103] Re-add except brackets --- 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 a935d2765..f125e6f0b 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -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: