From f8b98e0c0051b37a97a4c39ba947a2b50b43273c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Feb 2026 10:47:36 +0100 Subject: [PATCH 001/124] 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 fed70c92f94bcf44be5daa49a21c4362b1a2a87f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:19:52 +0100 Subject: [PATCH 002/124] 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 eaaa5468717f4584b9083855e5fc7a8bd488afcf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:43:00 +0100 Subject: [PATCH 003/124] 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 ebef512b85039effa0943c6e79c63e525e253bc9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:49:10 +0100 Subject: [PATCH 004/124] 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 1c4e2d86d2acf09817bb06f96f66443d6be0826f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 18:42:54 +0100 Subject: [PATCH 005/124] 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 ec1dcd615e09fa499eb72404069a6e9805dbcb27 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 19:29:26 +0100 Subject: [PATCH 006/124] 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 7b2a1aaf5764181b0b8f3095ca76b83e1bb2b123 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:11:42 +0100 Subject: [PATCH 007/124] 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 3301e24309ded8874144bab22d398557aaa130c4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:23:34 +0100 Subject: [PATCH 008/124] 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 bd5a22e66b2c7385eb10f8ee51146debb1d8edfb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:28:33 +0100 Subject: [PATCH 009/124] 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 3ce6d311ea9d721fea1b438fc1c7e9bfec1f300e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:31:31 +0100 Subject: [PATCH 010/124] 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 6cdbaa7bf62bc3d5fd8748fba87376a42efd717c 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/124] 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 f3e6fe30d5e890ba6c29f1ebacba0061dd404fe0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:06:35 +0100 Subject: [PATCH 012/124] 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 2c375a762294270937b790edd666c78b1304dc1f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:10:35 +0100 Subject: [PATCH 013/124] 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 9052b154aceb03ce80a52229507aa8267e81e103 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:19:39 +0100 Subject: [PATCH 014/124] 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 553191dd1db77030fd1bca63a7de8100272d7cf8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:32:20 +0100 Subject: [PATCH 015/124] 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 825ff7e771fe6bd442189eafb62d484b55788eee Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:40:48 +0100 Subject: [PATCH 016/124] 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 b7b2faa268c8c080c6441e224a666a25b9d1f49e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:18:14 +0100 Subject: [PATCH 017/124] 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 f16762d7cbfbc2707f98dc29b1f9af5f1fa8c060 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:51:31 +0100 Subject: [PATCH 018/124] 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 1d758d48e5aa3fdbe25c0d99afe4ea518b735a7e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:58:46 +0100 Subject: [PATCH 019/124] 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 3f996baef8d64e755f060b17f11bb4b63d1bb43a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:09:13 +0100 Subject: [PATCH 020/124] 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 3c84db22f2935ea51058999d0a344313c6b2b7ea Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:10:41 +0100 Subject: [PATCH 021/124] 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 d7ba9a3c55699236497c01143e7724f46d8e1add Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:12:44 +0100 Subject: [PATCH 022/124] 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 740cbb1217ba12560234d407ce31688814080614 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:26:33 +0100 Subject: [PATCH 023/124] Try --- plugwise_usb/messages/responses.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 7be3cf599..aa9a29c44 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -434,16 +434,21 @@ def __init__(self) -> None: super().__init__(b"0011") self._unknown1 = Int(0, length=2) self._network_online = Int(0, length=2) - self._mac_nc = String(None, length=16) - self._network_id = Int(0, 4, False) - self._unknown2 = Int(0, length=2) self._params += [ self._unknown1, self._network_online, - self._mac_nc, - self._network_id, - self._unknown2, - ] + ] + if self._network_online == 1: + self._mac_nc = String(None, length=16) + self._network_id = Int(0, 4, False) + self._unknown2 = Int(0, length=2) + self._params += [ + self._unknown1, + self._network_online, + self._mac_nc, + self._network_id, + self._unknown2, + ] @property def mac_network_controller(self) -> str: From 9d4f8eb7c43fab77d1766ec24830abb5b45c7db8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:35:33 +0100 Subject: [PATCH 024/124] Try 2 --- plugwise_usb/messages/responses.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index aa9a29c44..fd9adf378 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -438,17 +438,17 @@ def __init__(self) -> None: self._unknown1, self._network_online, ] - if self._network_online == 1: - self._mac_nc = String(None, length=16) - self._network_id = Int(0, 4, False) - self._unknown2 = Int(0, length=2) - self._params += [ - self._unknown1, - self._network_online, - self._mac_nc, - self._network_id, - self._unknown2, - ] +# if self._network_online == 1: +# self._mac_nc = String(None, length=16) +# self._network_id = Int(0, 4, False) +# self._unknown2 = Int(0, length=2) +# self._params += [ +# self._unknown1, +# self._network_online, +# self._mac_nc, +# self._network_id, +# self._unknown2, +# ] @property def mac_network_controller(self) -> str: From d98b55000cf8064e6b5a5b084c72c012f106d533 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:41:24 +0100 Subject: [PATCH 025/124] Try 3 --- plugwise_usb/messages/responses.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index fd9adf378..985b64914 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -437,7 +437,9 @@ def __init__(self) -> None: self._params += [ self._unknown1, self._network_online, - ] + ] + self._mac_nc = None + self._network_id = None # if self._network_online == 1: # self._mac_nc = String(None, length=16) # self._network_id = Int(0, 4, False) @@ -451,14 +453,18 @@ def __init__(self) -> None: # ] @property - def mac_network_controller(self) -> str: + def mac_network_controller(self) -> str | None: """Return the mac of the network controller (Circle+).""" # Replace first 2 characters by 00 for mac of circle+ node + if self._mac_nc is None: + return None return "00" + self._mac_nc.value[2:] @property - def network_id(self) -> int: + def network_id(self) -> int | None: """Return network ID.""" + if self._network_id is None: + return None return self._network_id.value @property From 793cb0fd0d981aec4f670805292ecc9cd7bea497 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:16:16 +0100 Subject: [PATCH 026/124] Add StickInitShortResponse --- plugwise_usb/messages/responses.py | 75 +++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 985b64914..c42a68d70 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -417,8 +417,45 @@ def __init__(self, timestamp: datetime | None = None) -> None: self._params += [self.image_timestamp] +class StickInitShortResponse(PlugwiseResponse): + """Returns the configuration and status of the USB-Stick - no network. + + Supported protocols : 1.0, 2.0 + Response to request : StickInitRequest + """ + + def __init__(self) -> None: + """Initialize StickInitShortResponse message object.""" + super().__init__(b"0011") + self._unknown1 = Int(0, length=2) + self._network_online = Int(0, length=2) + self._params += [ + self._unknown1, + self._network_online, + ] + + @property + def mac_network_controller(self) -> str | None: + """Return the mac of the network controller (Circle+).""" + return None + + @property + def network_id(self) -> int | None: + """Return network ID.""" + return None + + @property + def network_online(self) -> bool: + """Return state of network.""" + return self._network_online.value == 1 + + def __repr__(self) -> str: + """Convert request into writable str.""" + return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" + + class StickInitResponse(PlugwiseResponse): - """Returns the configuration and status of the USB-Stick. + """Returns the configuration and status of the USB-Stick - network online. Optional: - circle_plus_mac @@ -434,37 +471,26 @@ def __init__(self) -> None: super().__init__(b"0011") self._unknown1 = Int(0, length=2) self._network_online = Int(0, length=2) + self._mac_nc = String(None, length=16) + self._network_id = Int(0, 4, False) + self._unknown2 = Int(0, length=2) self._params += [ self._unknown1, self._network_online, + self._mac_nc, + self._network_id, + self._unknown2, ] - self._mac_nc = None - self._network_id = None -# if self._network_online == 1: -# self._mac_nc = String(None, length=16) -# self._network_id = Int(0, 4, False) -# self._unknown2 = Int(0, length=2) -# self._params += [ -# self._unknown1, -# self._network_online, -# self._mac_nc, -# self._network_id, -# self._unknown2, -# ] @property - def mac_network_controller(self) -> str | None: + def mac_network_controller(self) -> str: """Return the mac of the network controller (Circle+).""" # Replace first 2 characters by 00 for mac of circle+ node - if self._mac_nc is None: - return None return "00" + self._mac_nc.value[2:] @property - def network_id(self) -> int | None: + def network_id(self) -> int: """Return network ID.""" - if self._network_id is None: - return None return self._network_id.value @property @@ -1011,8 +1037,15 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 return NodePingResponse() if identifier == b"0010": return NodeImageValidationResponse() + + # 0011 has two formats if identifier == b"0011": - return StickInitResponse() + if length == 20: + return StickInitShortResponse() + if length == 42: + return StickInitResponse() + return None + if identifier == b"0013": return CirclePowerUsageResponse() if identifier == b"0015": From d7dc26232b1cf53865cc5882597eb0a346e2f2f5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:28:26 +0100 Subject: [PATCH 027/124] 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 60c108c9ac2aec5e2713da5a9799e4120613357e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:34:02 +0100 Subject: [PATCH 028/124] Update length --- plugwise_usb/messages/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index c42a68d70..abbbe59d4 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1040,9 +1040,9 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 # 0011 has two formats if identifier == b"0011": - if length == 20: + if length == 36: return StickInitShortResponse() - if length == 42: + if length == 60: return StickInitResponse() return None From 972bf35f3720cda3f71c7a0f80900575bf31c1d7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:41:44 +0100 Subject: [PATCH 029/124] Adapt StickInitRequest send() --- plugwise_usb/messages/requests.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 0f8c92f77..7d472c4a5 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -38,6 +38,7 @@ NodeSpecificResponse, PlugwiseResponse, StickInitResponse, + StickInitShortResponse, StickNetworkInfoResponse, StickResponse, StickResponseType, @@ -523,7 +524,7 @@ class StickInitRequest(PlugwiseRequest): """Initialize USB-Stick. Supported protocols : 1.0, 2.0 - Response message : StickInitResponse + Response message : StickInitResponse or StickInitShortResponse """ _identifier = b"000A" @@ -537,17 +538,17 @@ def __init__( super().__init__(send_fn, None) self._max_retries = 1 - async def send(self) -> StickInitResponse | None: + async def send(self) -> StickInitResponse | StickInitShortResponse | None: """Send request.""" if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, StickInitResponse): + if isinstance(result, StickInitResponse) or isinstance(result, StickInitShortResponse): return result if result is None: return None raise MessageError( - f"Invalid response message. Received {result.__class__.__name__}, expected StickInitResponse" + f"Invalid response message. Received {result.__class__.__name__}, expected StickInitResponse/StickInitShortResponse" ) From 7823804a4ea7cbb40f4508ca03bd001e0760161d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:49:23 +0100 Subject: [PATCH 030/124] Update length StickInitResponse --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index abbbe59d4..800c80ae7 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1042,7 +1042,7 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 if identifier == b"0011": if length == 36: return StickInitShortResponse() - if length == 60: + if length == 58: return StickInitResponse() return None From 7e77c1f61940736fc42e80be91e9ba565b45f56b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:57:19 +0100 Subject: [PATCH 031/124] 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 cd283e642e1cd5bd79fbe3b73a38c0fe2dbffcd6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:59:30 +0100 Subject: [PATCH 032/124] Full test-output - test_pairing --- scripts/tests_and_coverage.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index e3b7ee971..5c0ad8ac7 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,7 +56,8 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/ + # PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || + PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 15a30563844c3019550a0d2a7cebc1d2fd2d6caf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:11:30 +0100 Subject: [PATCH 033/124] 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 90d6ac6f7fcd512e67a119dc7278dbf91d0fbf64 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:13:36 +0100 Subject: [PATCH 034/124] 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 907a96317b9c4382ced9bcec9426f751f8b55dc9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:42:31 +0100 Subject: [PATCH 035/124] 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 ad1f25683023c932b893f4664784af4627b4dc02 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:46:12 +0100 Subject: [PATCH 036/124] 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 e9d2890da695a61facf3591bab9ba2e76255af10 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sun, 8 Feb 2026 12:49:30 +0000 Subject: [PATCH 037/124] fixup: pair-plus Python code fixed using Ruff --- plugwise_usb/messages/requests.py | 4 +- tests/test_pairing.py | 181 +++++++++++++++--------------- 2 files changed, 94 insertions(+), 91 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 7d472c4a5..3c23c4a14 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -543,7 +543,9 @@ async def send(self) -> StickInitResponse | StickInitShortResponse | None: if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, StickInitResponse) or isinstance(result, StickInitShortResponse): + if isinstance(result, StickInitResponse) or isinstance( + result, StickInitShortResponse + ): return result if result is None: return None diff --git a/tests/test_pairing.py b/tests/test_pairing.py index eb975b0e2..33f09339c 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -300,65 +300,65 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No """Callable dummy routine.""" return -# @pytest.mark.asyncio -# async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: -# """Test connecting to stick.""" -# monkeypatch.setattr( -# pw_connection_manager, -# "create_serial_connection", -# MockSerial(None).mock_connection, -# ) -# stick = pw_stick.Stick(port="test_port", cache_enabled=False) - -# unsub_connect = stick.subscribe_to_stick_events( -# stick_event_callback=self.connected, -# events=(pw_api.StickEvent.CONNECTED,), -# ) -# self.test_connected = asyncio.Future() -# await stick.connect("test_port") -# assert await self.test_connected -# await stick.initialize() -# assert stick.mac_stick == "0123456789012345" -# assert stick.name == "Stick 12345" -# assert stick.mac_coordinator == "0098765432101234" -# assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) -# assert stick.hardware == "070085" -# assert not stick.network_discovered -# assert stick.network_state -# assert stick.network_id == 17185 -# unsub_connect() -# await stick.disconnect() -# assert not stick.network_state -# with pytest.raises(pw_exceptions.StickError): -# stick.mac_stick - -# @pytest.mark.asyncio -# async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: -# """Testing Stick init without paired Circle.""" -# mock_serial = MockSerial( -# { -# b"\x05\x05\x03\x03000AB43C\r\n": ( -# "STICK INIT", -# b"000000C1", # Success ack -# b"0011" # response msg_id -# + b"0123456789012345" # stick mac -# + b"00" # unknown1 -# + b"00", # network_is_offline -# ), -# } -# ) -# monkeypatch.setattr( -# pw_connection_manager, -# "create_serial_connection", -# mock_serial.mock_connection, -# ) -# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) -# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) -# stick = pw_stick.Stick(port="test_port", cache_enabled=False) -# await stick.connect() -# with pytest.raises(pw_exceptions.StickError): -# await stick.initialize() -# await stick.disconnect() + # @pytest.mark.asyncio + # async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: + # """Test connecting to stick.""" + # monkeypatch.setattr( + # pw_connection_manager, + # "create_serial_connection", + # MockSerial(None).mock_connection, + # ) + # stick = pw_stick.Stick(port="test_port", cache_enabled=False) + + # unsub_connect = stick.subscribe_to_stick_events( + # stick_event_callback=self.connected, + # events=(pw_api.StickEvent.CONNECTED,), + # ) + # self.test_connected = asyncio.Future() + # await stick.connect("test_port") + # assert await self.test_connected + # await stick.initialize() + # assert stick.mac_stick == "0123456789012345" + # assert stick.name == "Stick 12345" + # assert stick.mac_coordinator == "0098765432101234" + # assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) + # assert stick.hardware == "070085" + # assert not stick.network_discovered + # assert stick.network_state + # assert stick.network_id == 17185 + # unsub_connect() + # await stick.disconnect() + # assert not stick.network_state + # with pytest.raises(pw_exceptions.StickError): + # stick.mac_stick + + # @pytest.mark.asyncio + # async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: + # """Testing Stick init without paired Circle.""" + # mock_serial = MockSerial( + # { + # b"\x05\x05\x03\x03000AB43C\r\n": ( + # "STICK INIT", + # b"000000C1", # Success ack + # b"0011" # response msg_id + # + b"0123456789012345" # stick mac + # + b"00" # unknown1 + # + b"00", # network_is_offline + # ), + # } + # ) + # monkeypatch.setattr( + # pw_connection_manager, + # "create_serial_connection", + # mock_serial.mock_connection, + # ) + # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) + # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) + # stick = pw_stick.Stick(port="test_port", cache_enabled=False) + # await stick.connect() + # with pytest.raises(pw_exceptions.StickError): + # await stick.initialize() + # await stick.disconnect() @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: @@ -375,13 +375,14 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.connect("test_port") with pytest.raises(pw_exceptions.StickError): await stick.initialize() - + await asyncio.sleep(5) await stick.plus_pair_request("0123456789012345") await asyncio.sleep(5) await stick.disconnect() + # async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] # """Handle join event callback.""" # if event == pw_api.NodeEvent.JOIN: @@ -393,33 +394,33 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: # ) # ) - # @pytest.mark.asyncio - # async def test_stick_node_join_subscription( - # self, monkeypatch: pytest.MonkeyPatch - # ) -> None: - # """Testing "new_node" subscription.""" - # mock_serial = MockSerial(None) - # monkeypatch.setattr( - # pw_connection_manager, - # "create_serial_connection", - # mock_serial.mock_connection, - # ) - # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) - # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) - # stick = pw_stick.Stick("test_port", cache_enabled=False) - # await stick.connect() - # await stick.initialize() - # await stick.discover_nodes(load=False) - - # self.test_node_join = asyncio.Future() - # unusb_join = stick.subscribe_to_node_events( - # node_event_callback=self.node_join, - # events=(pw_api.NodeEvent.JOIN,), - # ) - - ## Inject NodeJoinAvailableResponse - # mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! - # mac_join_node = await self.test_node_join - # assert mac_join_node == "9999999999999999" - # unusb_join() - # await stick.disconnect() +# @pytest.mark.asyncio +# async def test_stick_node_join_subscription( +# self, monkeypatch: pytest.MonkeyPatch +# ) -> None: +# """Testing "new_node" subscription.""" +# mock_serial = MockSerial(None) +# monkeypatch.setattr( +# pw_connection_manager, +# "create_serial_connection", +# mock_serial.mock_connection, +# ) +# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) +# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) +# stick = pw_stick.Stick("test_port", cache_enabled=False) +# await stick.connect() +# await stick.initialize() +# await stick.discover_nodes(load=False) + +# self.test_node_join = asyncio.Future() +# unusb_join = stick.subscribe_to_node_events( +# node_event_callback=self.node_join, +# events=(pw_api.NodeEvent.JOIN,), +# ) + +## Inject NodeJoinAvailableResponse +# mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! +# mac_join_node = await self.test_node_join +# assert mac_join_node == "9999999999999999" +# unusb_join() +# await stick.disconnect() From 914a3e67cd705217a8d02ce982e48d4d6ad196de Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:33:41 +0100 Subject: [PATCH 038/124] 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 6d0cb737c2d2fc6ac9cd795f4ac0a934bdc9abd2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:38:21 +0100 Subject: [PATCH 039/124] Use inheritance for StickInitResponse --- plugwise_usb/messages/responses.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 800c80ae7..9f620216e 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -454,7 +454,7 @@ def __repr__(self) -> str: return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" -class StickInitResponse(PlugwiseResponse): +class StickInitResponse(StickInitShortResponse): """Returns the configuration and status of the USB-Stick - network online. Optional: @@ -468,15 +468,11 @@ class StickInitResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize StickInitResponse message object.""" - super().__init__(b"0011") - self._unknown1 = Int(0, length=2) - self._network_online = Int(0, length=2) + super().__init__() self._mac_nc = String(None, length=16) self._network_id = Int(0, 4, False) self._unknown2 = Int(0, length=2) self._params += [ - self._unknown1, - self._network_online, self._mac_nc, self._network_id, self._unknown2, @@ -493,15 +489,6 @@ def network_id(self) -> int: """Return network ID.""" return self._network_id.value - @property - def network_online(self) -> bool: - """Return state of network.""" - return self._network_online.value == 1 - - def __repr__(self) -> str: - """Convert request into writable str.""" - return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" - class CirclePowerUsageResponse(PlugwiseResponse): """Returns power usage as impulse counters for several different time frames. From ea5c2fa534f2fa87661f3d3069a54913838db572 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:52:34 +0100 Subject: [PATCH 040/124] Move pair_plus_device() to connection --- plugwise_usb/__init__.py | 8 ++--- plugwise_usb/connection/__init__.py | 53 ++++++++++++++++++++++++++++- plugwise_usb/network/__init__.py | 51 --------------------------- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 2125368ca..fa4de8144 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -266,6 +266,10 @@ async def connect(self, port: str | None = None) -> None: self._port, ) + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + return await self._controller.pair_plus_device(mac) + @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" @@ -278,10 +282,6 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() - async def plus_pair_request(self, mac: str) -> bool: - """Send a pair request to a Plus device.""" - return await self._network.pair_plus_device(mac) - @raise_not_connected @raise_not_initialized async def start_network(self) -> None: diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index ff17f1400..fd19b4e8f 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -9,12 +9,14 @@ from ..api import StickEvent from ..constants import UTF8 from ..exceptions import NodeError, StickError -from ..helpers.util import version_to_model +from ..helpers.util import validate_mac, version_to_model from ..messages.requests import ( + CirclePlusConnectRequest, NodeInfoRequest, NodePingRequest, PlugwiseRequest, StickInitRequest, + StickNetworkInfoRequest, ) from ..messages.responses import ( NodeInfoResponse, @@ -202,6 +204,55 @@ async def initialize_stick(self) -> None: if not self._network_online: raise StickError("Zigbee network connection to Circle+ is down.") + async def pair_plus_device(self, mac: str) -> bool: + """Pair Plus-device to Plugwise Stick. + + According to https://roheve.wordpress.com/author/roheve/page/2/ + The pairing process should look like: + 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), + 000A - 0011: StickInitRequest - StickInitResponse, + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, + the Plus-device will then send a NodeRejoinResponse (0061). + + Todo(?): Does this need repeating until pairing is successful? + """ + _LOGGER.debug("Pair Plus-device with mac: %s", mac) + if not validate_mac(mac): + raise NodeError(f"Pairing failed: MAC {mac} invalid") + + # Collect network info + try: + request = StickNetworkInfoRequest(self.send, None) + info_response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if info_response is None: + raise NodeError( + "Pairing failed, StickNetworkInfoResponse is None" + ) from None + + # Init Stick + try: + await self.initialize_stick() + except StickError as exc: + raise NodeError( + f"Pairing failed, failed to initialize Stick: {exc}" + ) from exc + + try: + request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if response is None: + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") + + return True + async def get_node_details( self, mac: str, ping_first: bool ) -> tuple[NodeInfoResponse | None, NodePingResponse | None]: diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index aedc486dd..50834a751 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -22,9 +22,7 @@ from ..helpers.util import validate_mac from ..messages.requests import ( CircleMeasureIntervalRequest, - CirclePlusConnectRequest, NodePingRequest, - StickNetworkInfoRequest, ) from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, @@ -157,55 +155,6 @@ def registry(self) -> list[str]: # endregion - async def pair_plus_device(self, mac: str) -> bool: - """Pair Plus-device to Plugwise Stick. - - According to https://roheve.wordpress.com/author/roheve/page/2/ - The pairing process should look like: - 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), - 000A - 0011: StickInitRequest - StickInitResponse, - 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, - the Plus-device will then send a NodeRejoinResponse (0061). - - Todo(?): Does this need repeating until pairing is successful? - """ - _LOGGER.debug("Pair Plus-device with mac: %s", mac) - if not validate_mac(mac): - raise NodeError(f"Pairing failed: MAC {mac} invalid") - - # Collect network info - try: - request = StickNetworkInfoRequest(self._controller.send, None) - info_response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if info_response is None: - raise NodeError( - "Pairing failed, StickNetworkInfoResponse is None" - ) from None - - # Init Stick - try: - await self._controller.initialize_stick() - except StickError as exc: - raise NodeError( - f"Pairing failed, failed to initialize Stick: {exc}" - ) from exc - - try: - request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if response is None: - raise NodeError( - "Pairing failed, CirclePlusConnectResponse is None" - ) from None - if response.allowed.value != 1: - raise NodeError("Pairing failed, not allowed") - - return True - async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From 769be4b751a1edb69c5909a88690141719b371b2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:03:58 +0100 Subject: [PATCH 041/124] 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 6f725b70dec9c11f59d4fe5bfae7a5c6fb6fb335 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:08:47 +0100 Subject: [PATCH 042/124] 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 7d1021e2d332427ab3dc5fd4d48571fe9a92b4f0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:35:55 +0100 Subject: [PATCH 043/124] 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 2aa907b6c760c7b432c2a1e1731ecad36196555d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:46:23 +0100 Subject: [PATCH 044/124] 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 bee8d33f300b81aeb94bdfcce4ad8d0ab0188248 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:47:25 +0100 Subject: [PATCH 045/124] Add missing import --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index fd19b4e8f..58be25c4f 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -8,7 +8,7 @@ from ..api import StickEvent from ..constants import UTF8 -from ..exceptions import NodeError, StickError +from ..exceptions import MessageError, NodeError, StickError from ..helpers.util import validate_mac, version_to_model from ..messages.requests import ( CirclePlusConnectRequest, From f9608779e70f2ff957d8ec166b58f36e1ebcfb0a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:49:34 +0100 Subject: [PATCH 046/124] 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 2ec36734c60b8bd4711fae19eba22e6a7b6d942e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:52:43 +0100 Subject: [PATCH 047/124] 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 15bf735086fd3d87b2879a1b1e7f57a03c366ba4 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 9 Feb 2026 07:56:13 +0000 Subject: [PATCH 048/124] 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 4339c0dbc390a69c0eebc82833b4fad40f9d7944 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:08:32 +0100 Subject: [PATCH 049/124] Try 3 --- plugwise_usb/connection/__init__.py | 26 ++++++++++++++------------ tests/test_pairing.py | 6 ++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 58be25c4f..2df82314b 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -230,6 +230,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None + _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -238,18 +239,19 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - - try: - request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) - response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if response is None: - raise NodeError( - "Pairing failed, CirclePlusConnectResponse is None" - ) from None - if response.allowed.value != 1: - raise NodeError("Pairing failed, not allowed") + _LOGGER.debug("HOI Init done") + + #try: + # request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + # response = await request.send() + #except MessageError as exc: + # raise NodeError(f"Pairing failed: {exc}") from exc + #if response is None: + # raise NodeError( + # "Pairing failed, CirclePlusConnectResponse is None" + # ) from None + #if response.allowed.value != 1: + # raise NodeError("Pairing failed, not allowed") return True diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 889fbe063..9508b6dbd 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -431,8 +431,10 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - # await stick.plus_pair_request("0098765432101234") - # await asyncio.sleep(5) + + await asyncio.sleep(2) + await stick.plus_pair_request("0098765432101234") + await asyncio.sleep(2) await stick.disconnect() From 1d2cc6cec21d7609c5f4ab514947649ab8dcf6ac Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:15:32 +0100 Subject: [PATCH 050/124] Try 4 --- plugwise_usb/connection/__init__.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 2df82314b..507589cb0 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -241,17 +241,19 @@ async def pair_plus_device(self, mac: str) -> bool: ) from exc _LOGGER.debug("HOI Init done") - #try: - # request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) - # response = await request.send() - #except MessageError as exc: - # raise NodeError(f"Pairing failed: {exc}") from exc - #if response is None: - # raise NodeError( - # "Pairing failed, CirclePlusConnectResponse is None" - # ) from None - #if response.allowed.value != 1: - # raise NodeError("Pairing failed, not allowed") + try: + request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if response is None: + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") + + _LOGGER.debug("HOI PlusConnectRequest done") return True From 48113f76a4bdab9d636ab2a9628eaeab7c8035d7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:27:31 +0100 Subject: [PATCH 051/124] 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 332bba594b8c22b089a8e29e6eefb326c64045c8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:28:46 +0100 Subject: [PATCH 052/124] 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 d639b5bad14a0d14b618d41e838c612df72b67fc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:32:54 +0100 Subject: [PATCH 053/124] CirclePlusConnectReqyest: shorter args --- plugwise_usb/messages/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 3c23c4a14..0656e9cca 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,7 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"00000000000000000000" + args = b"000000000000000000" msg: bytes = self._identifier + args if self._mac is not None: msg += self._mac From b7ef23d8183d5d2237e148cc946b4380b619d8af Mon Sep 17 00:00:00 2001 From: autoruff Date: Tue, 10 Feb 2026 07:34:30 +0000 Subject: [PATCH 054/124] 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 9318f82adc18bcdb43e158c7cae92e1cdea0da56 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:04:26 +0100 Subject: [PATCH 055/124] 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 d748f8f4653f7937272cc9c99fb0b147a1e8d574 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:10:31 +0100 Subject: [PATCH 056/124] 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 4418e42a3becbcc5e0d1cb97634635fff50a3cf4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:31:16 +0100 Subject: [PATCH 057/124] Shorten args, must be length=16 --- plugwise_usb/messages/requests.py | 2 +- tests/test_pairing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 0656e9cca..484eefd2d 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,7 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"000000000000000000" + args = b"0000000000000000" msg: bytes = self._identifier + args if self._mac is not None: msg += self._mac diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 80c5a9513..4f219e749 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + b"\x05\x05\x03\x03000400000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From 2a5bbc1dac28655cc391314193a24ed5df63c22a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:47:09 +0100 Subject: [PATCH 058/124] 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 081ed2e9151626fbe7f4694f7c60ffb5c7c37252 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 12 Feb 2026 19:55:47 +0100 Subject: [PATCH 059/124] Try --- plugwise_usb/messages/requests.py | 2 +- tests/test_pairing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 484eefd2d..3c23c4a14 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,7 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"0000000000000000" + args = b"00000000000000000000" msg: bytes = self._identifier + args if self._mac is not None: msg += self._mac diff --git a/tests/test_pairing.py b/tests/test_pairing.py index d41969a3d..61b7dc249 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x03000400000000000000000098765432101234ABCD\r\n": ( + b"\x05\x05\x03\x030004000000000000000000000098765432101234ABCD\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From 36aab224d31b0323ba24be555a84cedaf183b688 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 07:59:09 +0100 Subject: [PATCH 060/124] 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 50b4e2c48bf0873542fe0cd7d7c3db87ea8b9c6d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:03:48 +0100 Subject: [PATCH 061/124] 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 e0c059929979f0d5c2411a6fde5cb3bace6222bd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:05:23 +0100 Subject: [PATCH 062/124] 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 779bc1921e056080f86ed03f1f87d3fadee88429 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:08:49 +0100 Subject: [PATCH 063/124] 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 3ad8384495e8dff99df2c2ae7c910291d8eeab87 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:35:20 +0100 Subject: [PATCH 064/124] 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 3057b335707cd43bf351d7fc29e8717623f0258a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:56:13 +0100 Subject: [PATCH 065/124] 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 b007149554c107a41ff28bde7b60fb2a1c309e0d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:15:14 +0100 Subject: [PATCH 066/124] Bump to v0.48.0a1 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0379b7971..9665f9035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.47.8" +version = "0.48.0a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 09410ebbfdf80a26fd1aa2ff150b1e2d46b66b9c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:17:07 +0100 Subject: [PATCH 067/124] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf26710c..1445a627d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ PR [422](https://github.com/plugwise/python-plugwise-usb/pull/422): Add missing - PR [418](https://github.com/plugwise/python-plugwise-usb/pull/418): Improve raise-message for better debugging - PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Fix recent Ruff errors +- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) ## v0.47.2 - 2026-01-29 - PR [400](https://github.com/plugwise/python-plugwise-usb/pull/400): Fix for Issue [#399](https://github.com/plugwise/python-plugwise-usb/issues/399) From 6a2a258c81053dd8a9164276d50052fd900e26e7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:41:52 +0100 Subject: [PATCH 068/124] Implement StickInitShortResponse-handling in class StickController --- plugwise_usb/connection/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 507589cb0..77e8e1c06 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -23,6 +23,7 @@ NodePingResponse, PlugwiseResponse, StickInitResponse, + StickInitShortResponse, ) from .manager import StickConnectionManager from .queue import StickQueue @@ -172,7 +173,9 @@ async def initialize_stick(self) -> None: try: request = StickInitRequest(self.send) - init_response: StickInitResponse | None = await request.send() + init_response: ( + StickInitResponse | StickInitShortResponse | None + ) = await request.send() except StickError as err: raise StickError( "No response from USB-Stick to initialization request." @@ -188,10 +191,11 @@ async def initialize_stick(self) -> None: self._mac_stick = init_response.mac_decoded self.stick_name = f"Stick {self._mac_stick[-5:]}" self._network_online = init_response.network_online + if self._network_online: + # Replace first 2 characters by 00 for mac of circle+ node + self._mac_nc = init_response.mac_network_controller + self._network_id = init_response.network_id - # Replace first 2 characters by 00 for mac of circle+ node - self._mac_nc = init_response.mac_network_controller - self._network_id = init_response.network_id self._is_initialized = True # Add Stick NodeInfoRequest @@ -201,9 +205,6 @@ async def initialize_stick(self) -> None: hardware, _ = version_to_model(node_info.hardware) self._hw_stick = hardware - if not self._network_online: - raise StickError("Zigbee network connection to Circle+ is down.") - async def pair_plus_device(self, mac: str) -> bool: """Pair Plus-device to Plugwise Stick. From bf47cbb77c092b68f427cc8745489232bba42d91 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:44:42 +0100 Subject: [PATCH 069/124] Back to full test-output --- scripts/tests_and_coverage.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 5c0ad8ac7..54fe2b020 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,8 +56,7 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - # PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || - PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py + PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 2b4f14576fc5e3e04775264fb383a82f6801229a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:22:04 +0100 Subject: [PATCH 070/124] Improve --- plugwise_usb/messages/requests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 3c23c4a14..b8a34b458 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -543,9 +543,7 @@ async def send(self) -> StickInitResponse | StickInitShortResponse | None: if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, StickInitResponse) or isinstance( - result, StickInitShortResponse - ): + if isinstance(result, (StickInitResponse, StickInitShortResponse)): return result if result is None: return None From b81b32047bdcd92cebb569715b5173e616a5b66b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:28:28 +0100 Subject: [PATCH 071/124] Correct CHANGELOG after rebase --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1445a627d..464190c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,9 @@ PR [422](https://github.com/plugwise/python-plugwise-usb/pull/422): Add missing ## v0.47.3 - 2026-03-04 -- PR [418](https://github.com/plugwise/python-plugwise-usb/pull/418): Improve raise-message for better debugging -- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Fix recent Ruff errors - +- PR [409](https://github.com/plugwise/python-plugwise-usb/pull/409): Fix recent Ruff errors - PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) + ## v0.47.2 - 2026-01-29 - PR [400](https://github.com/plugwise/python-plugwise-usb/pull/400): Fix for Issue [#399](https://github.com/plugwise/python-plugwise-usb/issues/399) From c8a0c3b79708ee969fc1a5a5f31f52d833a22753 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:29:04 +0100 Subject: [PATCH 072/124] Bump to v0.48.0a2 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9665f9035..24332a19f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a1" +version = "0.48.0a2" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 9aa3755daf67bcb7d15f8181019270ffe26756ff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:31:46 +0100 Subject: [PATCH 073/124] Run all test-files in case of failure --- scripts/tests_and_coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 54fe2b020..e3b7ee971 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,7 +56,7 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py + PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/ fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 0fd58d0df9dc0ba3ae16404d50ac01ae1bd306ec Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 10:45:38 +0100 Subject: [PATCH 074/124] Revert back to python 3.13 --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 710c318ff..da7ddc826 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -132,7 +132,7 @@ jobs: - commitcheck strategy: matrix: - python-version: ["3.14"] + python-version: ["3.13"] steps: - name: Check out committed code uses: actions/checkout@v7 From 4990de7c1e4cbfd1c159b86412e35620178abec6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 11:03:36 +0100 Subject: [PATCH 075/124] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 24332a19f..6c01be5a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a2" +version = "0.48.0a3" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 977e28a5049292ea9388770b69c23e5dd30b4326 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:33:49 +0100 Subject: [PATCH 076/124] 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 ffa30b8f82f0bfd19a0c135d03be7e61b659aa63 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:46:16 +0100 Subject: [PATCH 077/124] Ruff fix --- plugwise_usb/messages/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index b8a34b458..4b35a865d 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -543,7 +543,7 @@ async def send(self) -> StickInitResponse | StickInitShortResponse | None: if self._send_fn is None: raise MessageError("Send function missing") result = await self._send_request() - if isinstance(result, (StickInitResponse, StickInitShortResponse)): + if isinstance(result, StickInitResponse | StickInitShortResponse): return result if result is None: return None From 06a1ec2ae6ff811de833501f52e8f926d406dc12 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:48:49 +0100 Subject: [PATCH 078/124] 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 393d0a0b028acaf5bc58111396e3db06be820490 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:51:40 +0100 Subject: [PATCH 079/124] Bump to a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6c01be5a2..2de093fae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a3" +version = "0.48.0a4" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 293fe1a9b92a6b1f9b3fe14267669cd8edcc5d10 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:52:47 +0100 Subject: [PATCH 080/124] 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 b4aede097ef09d6afd5e6df10cf63772e7c402d6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:53:14 +0100 Subject: [PATCH 081/124] Bump to a5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2de093fae..df5c134c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a4" +version = "0.48.0a5" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 9c38f5d90fe2ed00ba2ac9ddf379c4ba2e56247f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 17:03:13 +0100 Subject: [PATCH 082/124] Disable now invalid test --- tests/test_usb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index b7668ac38..6b6399478 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -477,8 +477,8 @@ async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: unsub_connect() await stick.disconnect() assert not stick.network_state - with pytest.raises(pw_exceptions.StickError): - stick.mac_stick + # with pytest.raises(pw_exceptions.StickError): + # stick.mac_stick async def disconnected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined] """Handle disconnect event callback.""" From c06e67f063fce371007a3d2b3ff76a7bdd99b885 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:07 +0100 Subject: [PATCH 083/124] 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 9356dc359e2475737e34078abde43f593b00f62c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:26 +0100 Subject: [PATCH 084/124] Bump to a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df5c134c3..d8620c4c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a5" +version = "0.48.0a6" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 0dec00f836e9dbff2fcd55caa21613be15a3be0c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:05:34 +0100 Subject: [PATCH 085/124] 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 86114bd3896356de0c085d216a4da1db8594c3fd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:06:15 +0100 Subject: [PATCH 086/124] Bump to a7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d8620c4c2..4709efbc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a6" +version = "0.48.0a7" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From e77a7048be6d9cf610f90eca8a5b13415f6f3fc4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:08:54 +0100 Subject: [PATCH 087/124] 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 29c447824e1547014e0a86ba24184aac44824798 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:02:30 +0100 Subject: [PATCH 088/124] 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 d5ccf34cc05e3008f90d5bd20c3153277bac12b9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:04:26 +0100 Subject: [PATCH 089/124] Bump to a8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4709efbc7..b319fe595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a7" +version = "0.48.0a8" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 033844ad7e6e5b24f2fc69bea6722680b11c9a81 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:56:53 +0100 Subject: [PATCH 090/124] 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 5f191c6d10dcf3f3ea18f7cc2343f76dd98280a8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:01:58 +0100 Subject: [PATCH 091/124] Update relevant test-asserts --- tests/test_usb.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 6b6399478..5a0609f38 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -355,12 +355,9 @@ async def test_stick_connect_without_port(self) -> None: stick = pw_stick.Stick() assert stick.nodes == {} assert stick.joined_nodes is None - with pytest.raises(pw_exceptions.StickError): - stick.mac_stick - with pytest.raises(pw_exceptions.StickError): - stick.mac_coordinator - with pytest.raises(pw_exceptions.StickError): - stick.network_id + assert stick.mac_stick is None + assert stick.mac_coordinator is None + assert stick.network_id is None assert not stick.network_discovered assert not stick.network_state @@ -477,8 +474,6 @@ async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: unsub_connect() await stick.disconnect() assert not stick.network_state - # with pytest.raises(pw_exceptions.StickError): - # stick.mac_stick async def disconnected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined] """Handle disconnect event callback.""" From da58e7f7695927ffe45b8526c278d01a6432c4e9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:05:41 +0100 Subject: [PATCH 092/124] 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 3e843da033e65824cea2589acc5949371732d1c0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:24:55 +0100 Subject: [PATCH 093/124] Correct -update test_stick_network_down() --- tests/test_usb.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 5a0609f38..425c274dc 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1532,10 +1532,7 @@ async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None b"0011" # msg_id + b"0123456789012345" # stick mac + b"00" # unknown1 - + b"00" # network_is_online - + b"0098765432101234" # circle_plus_mac - + b"4321" # network_id - + b"00", # unknown2 + + b"00", # network_is_offline ), } ) @@ -1548,8 +1545,8 @@ async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect() - with pytest.raises(pw_exceptions.StickError): - await stick.initialize() + await stick.initialize() + assert stick.mac_coordinator is None await stick.disconnect() def fake_env(self, env: str) -> str | None: From ec2a59c65de1df8f739a3ddb632cd2dfa03d5147 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:29:50 +0100 Subject: [PATCH 094/124] Fix pylint warnings --- tests/test_usb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 425c274dc..8c922f0dc 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -194,9 +194,12 @@ async def mock_connection( loop: asyncio.AbstractEventLoop, protocol_factory: Callable[[], pw_receiver.StickReceiver], # type: ignore[name-defined] **kwargs: dict[str, Any], - ) -> tuple[DummyTransport, pw_receiver.StickReceiver]: # type: ignore[name-defined] + ) -> tuple[DummyTransport, pw_receiver.StickReceiver] | None: # type: ignore[name-defined] """Mock connection with dummy connection.""" self._protocol = protocol_factory() + if self._protocol is None: + return None + self._transport = DummyTransport(loop, self.custom_response) self._transport.protocol_data_received = self._protocol.data_received loop.call_soon_threadsafe(self._protocol.connection_made, self._transport) From 98da94d259158b4f4888de2a49530cb785d95cfd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:46:58 +0100 Subject: [PATCH 095/124] 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 742419d5504f167be723d21960504c65083ab73d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:12:14 +0100 Subject: [PATCH 096/124] Bump to a9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b319fe595..ab40344b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a8" +version = "0.48.0a9" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 01ef736d1126ee7f1be43e4bc2491dbe13afa0ab Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:40:51 +0100 Subject: [PATCH 097/124] 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 c3dff6d8baf80181bb89e2e14c62402033b62732 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:51:53 +0100 Subject: [PATCH 098/124] 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 81f63752439cf0ae77f68c7751080732dbcecea9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 12:20:46 +0100 Subject: [PATCH 099/124] 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 245e8a58132d0c75ff16b73f519e644920722883 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 14:21:56 +0100 Subject: [PATCH 100/124] Bump to a10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ab40344b7..c271daf43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a9" +version = "0.48.0a10" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 75a0cc93f06815d3e6b6c81e44a1666e24263ace Mon Sep 17 00:00:00 2001 From: autoruff Date: Fri, 20 Feb 2026 13:22:59 +0000 Subject: [PATCH 101/124] 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 aa3f0380e0fd81ebbb2a392f203fd8218be55251 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 09:46:34 +0100 Subject: [PATCH 102/124] Responses: line up Int() use --- plugwise_usb/messages/responses.py | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 9f620216e..30acd7c6b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -332,7 +332,7 @@ class NodeSpecificResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeSpecificResponse message object.""" super().__init__(b"0003") - self.status = Int(0, 4) + self.status = Int(0, length=4) self._params += [self.status] @@ -346,8 +346,8 @@ class CirclePlusConnectResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize CirclePlusConnectResponse message object.""" super().__init__(b"0005") - self.existing = Int(0, 2) - self.allowed = Int(0, 2) + self.existing = Int(0, length=2) + self.allowed = Int(0, length=2) self._params += [self.existing, self.allowed] @@ -500,13 +500,13 @@ class CirclePowerUsageResponse(PlugwiseResponse): def __init__(self, protocol_version: str = "2.3") -> None: """Initialize CirclePowerUsageResponse message object.""" super().__init__(b"0013") - self._pulse_1s = Int(0, 4) - self._pulse_8s = Int(0, 4) - self._nanosecond_offset = Int(0, 4) + self._pulse_1s = Int(0, length=4) + self._pulse_8s = Int(0, length=4) + self._nanosecond_offset = Int(0, length=4) self._params += [self._pulse_1s, self._pulse_8s] if protocol_version == "2.3": - self._pulse_counter_consumed = Int(0, 8) - self._pulse_counter_produced = Int(0, 8) + self._pulse_counter_consumed = Int(0, length=8) + self._pulse_counter_produced = Int(0, length=8) self._params += [ self._pulse_counter_consumed, self._pulse_counter_produced, @@ -608,7 +608,7 @@ def __init__(self) -> None: """Initialize NodeRemoveResponse message object.""" super().__init__(b"001D") self.node_mac_id = String(None, length=16) - self.status = Int(0, 2) + self.status = Int(0, length=2) self._params += [self.node_mac_id, self.status] @@ -757,8 +757,8 @@ def __init__(self) -> None: super().__init__(b"003F") self.time = Time() self.day_of_week = Int(0, 2, False) - self.unknown = Int(0, 2) - self.unknown2 = Int(0, 4) + self.unknown = Int(0, length=2) + self.unknown2 = Int(0, length=4) self._params += [ self.time, self.day_of_week, @@ -779,13 +779,13 @@ def __init__(self) -> None: """Initialize CircleEnergyLogsResponse message object.""" super().__init__(b"0049") self.logdate1 = DateTime() - self.pulses1 = Int(0, 8) + self.pulses1 = Int(0, length=8) self.logdate2 = DateTime() - self.pulses2 = Int(0, 8) + self.pulses2 = Int(0, length=8) self.logdate3 = DateTime() - self.pulses3 = Int(0, 8) + self.pulses3 = Int(0, length=8) self.logdate4 = DateTime() - self.pulses4 = Int(0, 8) + self.pulses4 = Int(0, length=8) self._logaddr = LogAddr(0, length=8) self._params += [ self.logdate1, @@ -969,7 +969,7 @@ class SenseReportResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize SenseReportResponse message object.""" super().__init__(SENSE_REPORT_ID) - self.humidity = Int(0, length=4, negative=False) + self.humidity = Int(0, 4, False) self.temperature = Int(0, length=4) self._params += [self.humidity, self.temperature] From 54695e6cf5fa52f6fbc16e67d27acc52352640cf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 09:59:36 +0100 Subject: [PATCH 103/124] Add missing decode_mac=False --- plugwise_usb/messages/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 30acd7c6b..7d1fa6481 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -288,7 +288,7 @@ class StickNetworkInfoResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeNetworkInfoResponse message object.""" - super().__init__(b"0002") + super().__init__(b"0002", decode_mac=False) self._channel = Int(0, length=2) self._source_mac_id = String(None, length=16) self.extended_pan_id = String(None, length=16) @@ -331,7 +331,7 @@ class NodeSpecificResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeSpecificResponse message object.""" - super().__init__(b"0003") + super().__init__(b"0003", decode_mac=False) self.status = Int(0, length=4) self._params += [self.status] From 845b4f631f90fad2176be05d95a165c976b9f769 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 10:26:34 +0100 Subject: [PATCH 104/124] Correct 0002-format in response --- tests/test_pairing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index bf1037497..27da7b99f 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -47,7 +47,6 @@ "Stick network info request", b"000000C1", # Success ack b"0002" # response msg_id - + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" + b"FF98765432101234" # 06 + plus-device mac From 29e34311afeaf1cdf912adc7076869c0b1483356 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 10:47:55 +0100 Subject: [PATCH 105/124] Bump to a11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c271daf43..0f9e47749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a10" +version = "0.48.0a11" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 17e5a66f21eebc8a57011aff1638761ab5f8108f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 11:18:38 +0100 Subject: [PATCH 106/124] Exit when network is not online --- plugwise_usb/connection/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index a935d2765..ab9467004 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,7 +220,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None - _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -229,7 +228,8 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - _LOGGER.debug("HOI Init done") + if not self._network_online: + raise NodeError("Pairing failed, network is not online") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,8 +243,6 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") - _LOGGER.debug("HOI PlusConnectRequest done") - return True async def get_node_details( From 754d645f2472e9af735431d468c2ea48eaec9919 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 11:24:56 +0100 Subject: [PATCH 107/124] Bump to a12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f9e47749..f7774c9af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a11" +version = "0.48.0a12" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 98d29c99521d69f6e489f434ca6785a955aa4489 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:07:07 +0100 Subject: [PATCH 108/124] Revert "Exit when network is not online" This reverts commit 9d5ba855f5ba5c2dbed599b452f00382ee62157e. --- plugwise_usb/connection/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index ab9467004..a935d2765 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,6 +220,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None + _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -228,8 +229,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - if not self._network_online: - raise NodeError("Pairing failed, network is not online") + _LOGGER.debug("HOI Init done") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,6 +243,8 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") + _LOGGER.debug("HOI PlusConnectRequest done") + return True async def get_node_details( From a352da5bf77d1a14ecb1d5eb9d0798863eb958a2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:08:38 +0100 Subject: [PATCH 109/124] Remove logger-HOI-lines --- plugwise_usb/connection/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index a935d2765..2f89f1059 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,7 +220,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None - _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -229,7 +228,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - _LOGGER.debug("HOI Init done") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,8 +241,6 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") - _LOGGER.debug("HOI PlusConnectRequest done") - return True async def get_node_details( From 3bc0cf9fdfb6f43488aa333c9503141cd899e968 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:18:43 +0100 Subject: [PATCH 110/124] Bump to a13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f7774c9af..5e9fdb3db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a12" +version = "0.48.0a13" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From c26bd320d77e0161975c8e1226f07b8c71b01a65 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 15:28:44 +0100 Subject: [PATCH 111/124] Don't collect NodeInfo during pairing --- plugwise_usb/connection/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 2f89f1059..640eec4ce 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -151,7 +151,7 @@ async def _handle_stick_event(self, event: StickEvent) -> None: elif event == StickEvent.DISCONNECTED and self._queue.is_running: await self._queue.stop() - async def initialize_stick(self) -> None: + async def initialize_stick(self, node_info=True) -> None: """Initialize connection to the USB-stick.""" if not self._manager.is_connected: raise StickError( @@ -187,7 +187,10 @@ async def initialize_stick(self) -> None: self._is_initialized = True - # Add Stick NodeInfoRequest + if not node_info: + return + + # Collect Stick NodeInfo node_info, _ = await self.get_node_details(self._mac_stick, ping_first=False) if node_info is not None: self._fw_stick = node_info.firmware # type: ignore @@ -223,7 +226,7 @@ async def pair_plus_device(self, mac: str) -> bool: # Init Stick try: - await self.initialize_stick() + await self.initialize_stick(node_info=False) except StickError as exc: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" From 77093e6835a5d7aff5e46fff540516e09a6164d4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 16:23:21 +0100 Subject: [PATCH 112/124] Update 0004-request --- plugwise_usb/messages/requests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 4b35a865d..9c338b836 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,8 +406,9 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - args = b"00000000000000000000" - msg: bytes = self._identifier + args + special_id = b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" + args = b"0000000000000000" + msg: bytes = self._identifier + special_id + args if self._mac is not None: msg += self._mac checksum = self.calculate_checksum(msg) From 3a9a668580807c9c54431a95c6a297b66eeb01c1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 16:36:04 +0100 Subject: [PATCH 113/124] Update test-related --- tests/test_pairing.py | 2 +- tests/test_usb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 27da7b99f..a6c33adec 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -66,7 +66,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x0300040000000000000000000000987654321012344D73\r\n": ( + b"\x05\x05\x03\x030004000100000000000000000098765432101234C0AF\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id diff --git a/tests/test_usb.py b/tests/test_usb.py index 8c922f0dc..7320a7ea0 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1447,7 +1447,7 @@ async def test_creating_request_messages(self) -> None: ) assert ( circle_plus_connect_request.serialize() - == b"\x05\x05\x03\x030004000000000000000000001111222233334444BDEC\r\n" + == b"\x05\x05\x03\x0300040001000000000000000011112222333344443030\r\n" ) node_add_request = pw_requests.NodeAddRequest( self.dummy_fn, b"1111222233334444", True From ad286029fa5438707fae39019c0a7d4af10fd5bc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 18:59:09 +0100 Subject: [PATCH 114/124] Ruffed --- plugwise_usb/messages/requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 9c338b836..d77ba7976 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -406,7 +406,9 @@ def serialize(self) -> bytes: # key, byte # network info.index, ulong # network key = 0 - special_id = b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" + special_id = ( + b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" + ) args = b"0000000000000000" msg: bytes = self._identifier + special_id + args if self._mac is not None: From 3e20a7e3e8f48e2f71fa6e2bf709a005a68753ae Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 19:06:30 +0100 Subject: [PATCH 115/124] Update docstring --- plugwise_usb/messages/requests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index d77ba7976..e73f1adbb 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -400,15 +400,15 @@ async def send(self) -> CirclePlusConnectResponse | None: # This message has an exceptional format and therefore # need to override the serialize method def serialize(self) -> bytes: - """Convert message to serialized list of bytes.""" - # This command has - # args: byte - # key, byte - # network info.index, ulong - # network key = 0 - special_id = ( - b"0001" # observed sequence with retries: b"0000", b"0001", B"0101" - ) + """Convert message to serialized list of bytes. + + Parameters + ---------- + - special_id: byte - observed sequence with retry: b"0001", B"0101", + - and args: byte. + + """ + special_id = b"0001" args = b"0000000000000000" msg: bytes = self._identifier + special_id + args if self._mac is not None: From fe0edcba77774317e25a27b08dd210534c4d7cba Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 08:59:41 +0100 Subject: [PATCH 116/124] Bump to a14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5e9fdb3db..46390ddbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.48.0a13" +version = "0.48.0a14" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From fa88ec784308ceb4ee09852bfcece94122ea4ec1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 09:29:53 +0100 Subject: [PATCH 117/124] 0005-response: add missing decode_mac=False --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 7d1fa6481..998871d8b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -345,7 +345,7 @@ class CirclePlusConnectResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize CirclePlusConnectResponse message object.""" - super().__init__(b"0005") + super().__init__(b"0005", decode_mac=False) self.existing = Int(0, length=2) self.allowed = Int(0, length=2) self._params += [self.existing, self.allowed] From 7129a63239e3ac00cbed0de2807d3901dc8b4999 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 09:47:33 +0100 Subject: [PATCH 118/124] Improve docstring --- plugwise_usb/connection/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 640eec4ce..59ceab20e 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -202,12 +202,16 @@ async def pair_plus_device(self, mac: str) -> bool: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), - 000A - 0011: StickInitRequest - StickInitResponse, + 0001 - 0002 - 0003: StickNetworkInfoRequest - StickNetworkInfoResponse - NodeSpecificResponse, + 000A - 0011: StickInitRequest - StickInitShortResponse/StickInitResponse, 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, the Plus-device will then send a NodeRejoinResponse (0061). - Todo(?): Does this need repeating until pairing is successful? + In the first occurrence of this process a 0004 0001 .... message is sent. + A StickInitShortResponse is received indicating the network is offline. + In the second occurrence of this process a 0004 0101 .... message is sent. + Again a StickInitShortResponse is received. + In the third occurrence only 000A is sent and a StickInitResponse indicating the network is online, is received. """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From e170a24655bbaf81717412e9add5ac118c8bd0c2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Mar 2026 18:28:32 +0100 Subject: [PATCH 119/124] Adapt 0005-test-response --- tests/test_pairing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index a6c33adec..f62398c7d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -70,7 +70,6 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id - + b"0098765432101234" # circle+ mac + b"00" # existing + b"01", # allowed ), From 6f32e0e5993305c00c8b869d4b28f2d064a433aa Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 3 Mar 2026 08:23:51 +0100 Subject: [PATCH 120/124] Add init to StickNetworkInfoRequest --- plugwise_usb/messages/requests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index e73f1adbb..ec14e83e3 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -364,8 +364,18 @@ class StickNetworkInfoRequest(PlugwiseRequest): _identifier = b"0001" _reply_identifier = b"0002" + def __init__( + self, + send_fn: Callable[[PlugwiseRequest, bool], Awaitable[PlugwiseResponse | None]], + ) -> None: + """Initialize StickInitRequest message object.""" + super().__init__(send_fn, None) + self._max_retries = 1 + async def send(self) -> StickNetworkInfoResponse | None: """Send request.""" + if self._send_fn is None: + raise MessageError("Send function missing") result = await self._send_request() if isinstance(result, StickNetworkInfoResponse): return result From b9837225dc4b8271d97e5096268edfca1f7776cc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 3 Mar 2026 08:24:48 +0100 Subject: [PATCH 121/124] And adapt use --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 59ceab20e..aaa8f5748 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -219,7 +219,7 @@ async def pair_plus_device(self, mac: str) -> bool: # Collect network info try: - request = StickNetworkInfoRequest(self.send, None) + request = StickNetworkInfoRequest(self.send) info_response = await request.send() except MessageError as exc: raise NodeError(f"Pairing failed: {exc}") from exc From b351ee0ba362751d69e4b37f2c38e00e60df11fb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Mar 2026 20:02:48 +0100 Subject: [PATCH 122/124] Disable guarding that breaks 0004-0005 sequence detection --- plugwise_usb/connection/receiver.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/connection/receiver.py b/plugwise_usb/connection/receiver.py index 1dee3f8a4..6cf9103da 100644 --- a/plugwise_usb/connection/receiver.py +++ b/plugwise_usb/connection/receiver.py @@ -441,11 +441,11 @@ async def _notify_node_response_subscribers( notify_tasks: list[Coroutine[Any, Any, bool]] = [] for node_subscription in self._node_response_subscribers.values(): - if ( - node_subscription.mac is not None - and node_subscription.mac != node_response.mac - ): - continue + # if ( + # node_subscription.mac is not None + # and node_subscription.mac != node_response.mac + # ): + # continue if ( node_subscription.response_ids is not None and node_response.identifier not in node_subscription.response_ids From 35d9ce5e7045ca196409ca83f8b8bb8a1e2c5689 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Mar 2026 20:11:51 +0100 Subject: [PATCH 123/124] Fix CHANGELOG after rebase --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464190c81..b76d9b4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Ongoing + +- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) + ## v0.47.8 - 2026-07-01 PR [459](https://github.com/plugwise/python-plugwise-usb/pull/459): Reduce logging for SEDs @@ -22,8 +26,8 @@ PR [422](https://github.com/plugwise/python-plugwise-usb/pull/422): Add missing ## v0.47.3 - 2026-03-04 +- PR [418](https://github.com/plugwise/python-plugwise-usb/pull/418): Improve raise-message for better debugging - PR [409](https://github.com/plugwise/python-plugwise-usb/pull/409): Fix recent Ruff errors -- PR [405](https://github.com/plugwise/python-plugwise-usb/pull/405): Try adding plus-device pairing (untested!!) ## v0.47.2 - 2026-01-29 From 68c7206b480e5762a5a48f481b630220cb44e20b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 1 Sep 2026 19:29:07 +0200 Subject: [PATCH 124/124] Update pair_plus_device() docsting describing the recorded pairing process --- plugwise_usb/connection/__init__.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index aaa8f5748..4574fdbab 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -200,18 +200,16 @@ async def initialize_stick(self, node_info=True) -> 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/ The pairing process should look like: - 0001 - 0002 - 0003: StickNetworkInfoRequest - StickNetworkInfoResponse - NodeSpecificResponse, - 000A - 0011: StickInitRequest - StickInitShortResponse/StickInitResponse, - 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, - the Plus-device will then send a NodeRejoinResponse (0061). - - In the first occurrence of this process a 0004 0001 .... message is sent. - A StickInitShortResponse is received indicating the network is offline. - In the second occurrence of this process a 0004 0101 .... message is sent. - Again a StickInitShortResponse is received. - In the third occurrence only 000A is sent and a StickInitResponse indicating the network is online, is received. + Repeat sending 0001 (& 000A) until 0002 response is receiced + 0001 -> 0003, 000A -> 0011S, 0001 -> 0003, 000A - 0011S, 0001 -> 0002 & 0003, 000A - 0011S + 0004 0001 -> 0005, 0001 -> 0002, 000A -> 0011S + ~30s later: 0004 0101 -> 0005, 000A -> 0011 (full StickInitResponse means pairing succesful) + Follow up requesting NodeInfo + + 0001 - 0002 - 0003: StickNetworkInfoRequest - StickNetworkInfoResponse - NodeSpecificResponse + 000A - 0011S/0011: StickInitRequest - StickInitShortResponse/StickInitResponse + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, he Plus-device will then send a NodeRejoinResponse (0061). """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac):