From d20be8294b752f999509aeb2726f180b89d9bf32 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Feb 2026 10:47:36 +0100 Subject: [PATCH 001/123] Add plus_pair_request function --- plugwise_usb/__init__.py | 8 ++++++++ plugwise_usb/network/__init__.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index e3cc2c6b3..eebcb9258 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -176,6 +176,14 @@ def port(self, port: str) -> None: self._port = port + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + try: + await self._network.pair_plus_device(mac) + except NodeError as exc: + raise NodeError(f"{exc}") from exc + return True + async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 985cd7581..707762495 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -152,6 +152,21 @@ def registry(self) -> list[str]: # endregion + aync def pair_plus_device(self, mac: str) -> bool: + """Register node to Plugwise network.""" + _LOGGER.debug("Pair Plus-device with mac: %s", mac) + if not validate_mac(mac): + raise NodeError(f"MAC {mac} invalid") + + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) + if (response := await request.send()) is None: + raise NodeError("No response for CirclePlusConnectRequest.") + + # how do we check for a succesfull pairing? + # there should be a 0005 wxyz 0001 response + # followed by a StickInitResponse (0011)? + + async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From 210d414be34650fa3d0dbc29808571dde09baa25 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:19:52 +0100 Subject: [PATCH 002/123] Document pairing process --- plugwise_usb/network/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 707762495..91f496889 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -153,7 +153,15 @@ def registry(self) -> list[str]: # endregion aync def pair_plus_device(self, mac: str) -> bool: - """Register node to Plugwise network.""" + """Register node to Plugwise network. + + According to https://roheve.wordpress.com/author/roheve/page/2/ + The pairing process should look like: + 0001 - 0002 + 000A - 0011 + 0004 - 0005 & 0061 (NodeJoinResponse) + The NodeJoinRespons should trigger... + """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): raise NodeError(f"MAC {mac} invalid") From 29113e41b85b5342f783acf464abc753454c32c8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:43:00 +0100 Subject: [PATCH 003/123] Add 0001-0002 req-resp-pair --- plugwise_usb/network/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 91f496889..2beea9ae3 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -20,7 +20,11 @@ ) from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac -from ..messages.requests import CircleMeasureIntervalRequest, NodePingRequest +from ..messages.requests import ( + CircleMeasureIntervalRequest, + NodePingRequest, + StickNetworkInfoRequest, +) from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, NODE_JOIN_ID, @@ -30,6 +34,7 @@ NodeRejoinResponse, NodeResponseType, PlugwiseResponse, + StickNetworkInfoResponse, ) from ..nodes import get_plugwise_node from .registry import StickNetworkRegister @@ -166,6 +171,14 @@ def registry(self) -> list[str]: if not validate_mac(mac): raise NodeError(f"MAC {mac} invalid") + # Collect network info + request = StickNetworkInfoRequest(self._controller.send, None) + if (response := await request.send()) is not None: + if not isinstance(response, StickNetworkInfoResponse): + raise MessageError( + f"Invalid response message type ({response.__class__.__name__}) received, expected StickNetworkInfoResponse" + ) + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) if (response := await request.send()) is None: raise NodeError("No response for CirclePlusConnectRequest.") From 13a5b134a594b82ccf04ed8bc139178db357c8a9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 11:49:10 +0100 Subject: [PATCH 004/123] Add init stick --- plugwise_usb/network/__init__.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 2beea9ae3..335078958 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -173,12 +173,29 @@ def registry(self) -> list[str]: # Collect network info request = StickNetworkInfoRequest(self._controller.send, None) - if (response := await request.send()) is not None: - if not isinstance(response, StickNetworkInfoResponse): + if (info_response := await request.send()) is not None: + if not isinstance(info_response, StickNetworkInfoResponse): raise MessageError( - f"Invalid response message type ({response.__class__.__name__}) received, expected StickNetworkInfoResponse" + f"Invalid response message type ({info_response.__class__.__name__}) received, expected StickNetworkInfoResponse" ) + # Init Stick + try: + request = StickInitRequest(self._controller.send) + init_response: StickInitResponse | None = await request.send() + except StickError as err: + raise StickError( + "No response from USB-Stick to initialization request." + + " Validate USB-stick is connected to port " + + f"' {self._manager.serial_path}'" + ) from err + if init_response is None: + raise StickError( + "No response from USB-Stick to initialization request." + + " Validate USB-stick is connected to port " + + f"' {self._manager.serial_path}'" + ) + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) if (response := await request.send()) is None: raise NodeError("No response for CirclePlusConnectRequest.") From 8fb42a05d098d982009194e9539f9b50b01bf8c4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 18:42:54 +0100 Subject: [PATCH 005/123] Improve docstring --- plugwise_usb/network/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 335078958..603ae0a0d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -162,10 +162,10 @@ def registry(self) -> list[str]: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002 - 000A - 0011 - 0004 - 0005 & 0061 (NodeJoinResponse) - The NodeJoinRespons should trigger... + 0001 - 0002: StickNetworkInfoRequest - StickNetworkInfoResponse + 000A - 0011: StickInitRequest - StickInitResponse + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse + The Plus-device will then send a NodeRejoinResponse (0061). """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From 9e79012fedd86c40b633c7742af6c9d820d04b61 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 19:29:26 +0100 Subject: [PATCH 006/123] Improve pair_plus_device() --- plugwise_usb/network/__init__.py | 49 +++++++++++++------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 603ae0a0d..72c8faf1d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -169,41 +169,32 @@ def registry(self) -> list[str]: """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): - raise NodeError(f"MAC {mac} invalid") + raise NodeError(f"Pairing failed: MAC {mac} invalid") # Collect network info - request = StickNetworkInfoRequest(self._controller.send, None) - if (info_response := await request.send()) is not None: - if not isinstance(info_response, StickNetworkInfoResponse): - raise MessageError( - f"Invalid response message type ({info_response.__class__.__name__}) received, expected StickNetworkInfoResponse" - ) + try: + request = StickNetworkInfoRequest(self._controller.send, None) + info_response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") + if info_response is None: + raise NodeError("Pairing failed, StickNetworkInfoResponse is None") # Init Stick try: - request = StickInitRequest(self._controller.send) - init_response: StickInitResponse | None = await request.send() - except StickError as err: - raise StickError( - "No response from USB-Stick to initialization request." - + " Validate USB-stick is connected to port " - + f"' {self._manager.serial_path}'" - ) from err - if init_response is None: - raise StickError( - "No response from USB-Stick to initialization request." - + " Validate USB-stick is connected to port " - + f"' {self._manager.serial_path}'" - ) - - request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - if (response := await request.send()) is None: - raise NodeError("No response for CirclePlusConnectRequest.") + await self._controller.initialize_stick() + except StickError as exc: + raise NodeError(f"Pairing failed, failed to initialize Stick: {exc}") - # how do we check for a succesfull pairing? - # there should be a 0005 wxyz 0001 response - # followed by a StickInitResponse (0011)? - + try: + request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) + response = await request.send()) + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") + if response is None: + raise NodeError("Pairing failed, CirclePlusConnectResponse is None") + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" From 2dfad1c1e4b5224bf940cc69fa5173c1c908c2b9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:11:42 +0100 Subject: [PATCH 007/123] Add todo for maybe needed functionality --- plugwise_usb/network/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 72c8faf1d..c7e888f82 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -166,6 +166,8 @@ def registry(self) -> list[str]: 000A - 0011: StickInitRequest - StickInitResponse 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse The Plus-device will then send a NodeRejoinResponse (0061). + + Todo(?): Does this need repeating until pairing is succesful? """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From e9f2e0b86e11671373e9d5e2755647da8308c09a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:23:34 +0100 Subject: [PATCH 008/123] Fix typos, return type --- plugwise_usb/network/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index c7e888f82..622034a2d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -157,7 +157,7 @@ def registry(self) -> list[str]: # endregion - aync def pair_plus_device(self, mac: str) -> bool: + async def pair_plus_device(self, mac: str) -> None: """Register node to Plugwise network. According to https://roheve.wordpress.com/author/roheve/page/2/ @@ -190,7 +190,7 @@ def registry(self) -> list[str]: try: request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - response = await request.send()) + response = await request.send() except MessageError as exc: raise NodeError(f"Pairing failed: {exc}") if response is None: From 841332fdccc6b332037233959e16cf9c4ed79dba Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:28:33 +0100 Subject: [PATCH 009/123] Correct imports, improve docstring --- plugwise_usb/network/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 622034a2d..77176557e 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -21,6 +21,7 @@ from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac from ..messages.requests import ( + CirclePlusConnectRequest, CircleMeasureIntervalRequest, NodePingRequest, StickNetworkInfoRequest, @@ -34,7 +35,6 @@ NodeRejoinResponse, NodeResponseType, PlugwiseResponse, - StickNetworkInfoResponse, ) from ..nodes import get_plugwise_node from .registry import StickNetworkRegister @@ -158,8 +158,8 @@ def registry(self) -> list[str]: # endregion async def pair_plus_device(self, mac: str) -> None: - """Register node to Plugwise network. - + """Pair Plus-device to Plugwise Stick. + According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: 0001 - 0002: StickNetworkInfoRequest - StickNetworkInfoResponse From fccd283ba6346d1c3b1a75010db25b005835760b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 7 Feb 2026 20:31:31 +0100 Subject: [PATCH 010/123] Ruff fixes --- plugwise_usb/network/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 77176557e..d55e1f5fc 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -21,8 +21,8 @@ from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac from ..messages.requests import ( - CirclePlusConnectRequest, CircleMeasureIntervalRequest, + CirclePlusConnectRequest, NodePingRequest, StickNetworkInfoRequest, ) @@ -178,23 +178,29 @@ async def pair_plus_device(self, mac: str) -> None: request = StickNetworkInfoRequest(self._controller.send, None) info_response = await request.send() except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") + raise NodeError(f"Pairing failed: {exc}") from exc if info_response is None: - raise NodeError("Pairing failed, StickNetworkInfoResponse is None") + raise NodeError( + "Pairing failed, StickNetworkInfoResponse is None" + ) from None # Init Stick try: await self._controller.initialize_stick() except StickError as exc: - raise NodeError(f"Pairing failed, failed to initialize Stick: {exc}") + raise NodeError( + f"Pairing failed, failed to initialize Stick: {exc}" + ) from exc try: request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) response = await request.send() except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") + raise NodeError(f"Pairing failed: {exc}") from exc if response is None: - raise NodeError("Pairing failed, CirclePlusConnectResponse is None") + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") From 98d8e9b417a473ca3a597e91cfaba306ea2ad18f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 8 Feb 2026 08:04:59 +0000 Subject: [PATCH 011/123] Make sure the Stick is ready to pair, as suggested Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- plugwise_usb/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index eebcb9258..dc3426142 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -176,8 +176,12 @@ def port(self, port: str) -> None: self._port = port + `@raise_not_connected` + `@raise_not_initialized` async def plus_pair_request(self, mac: str) -> bool: """Send a pair request to a Plus device.""" + if self._network is None: + raise StickError("Cannot pair when network is not initialized") try: await self._network.pair_plus_device(mac) except NodeError as exc: From 855d64921fb3759474983bdd7449e5ded44bf6ed Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:06:35 +0100 Subject: [PATCH 012/123] Fix spelling --- plugwise_usb/network/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index d55e1f5fc..8c9b0e695 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -167,7 +167,7 @@ async def pair_plus_device(self, mac: str) -> None: 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse The Plus-device will then send a NodeRejoinResponse (0061). - Todo(?): Does this need repeating until pairing is succesful? + Todo(?): Does this need repeating until pairing is successful? """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From 94865bbd4b704799de5d48eba77c472c34724976 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:10:35 +0100 Subject: [PATCH 013/123] Remove quotes, move --- plugwise_usb/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index dc3426142..0d5e9040a 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -176,18 +176,6 @@ def port(self, port: str) -> None: self._port = port - `@raise_not_connected` - `@raise_not_initialized` - async def plus_pair_request(self, mac: str) -> bool: - """Send a pair request to a Plus device.""" - if self._network is None: - raise StickError("Cannot pair when network is not initialized") - try: - await self._network.pair_plus_device(mac) - except NodeError as exc: - raise NodeError(f"{exc}") from exc - return True - async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: @@ -290,6 +278,18 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() + @raise_not_connected + @raise_not_initialized + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + if self._network is None: + raise StickError("Cannot pair when network is not initialized") + try: + await self._network.pair_plus_device(mac) + except NodeError as exc: + raise NodeError(f"{exc}") from exc + return True + @raise_not_connected @raise_not_initialized async def start_network(self) -> None: From 1a96f09c7217128fff14872c60a2365f6a218982 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:19:39 +0100 Subject: [PATCH 014/123] Set output as bool and use --- plugwise_usb/__init__.py | 7 ++----- plugwise_usb/network/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 0d5e9040a..6bac7da7d 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -284,11 +284,8 @@ async def plus_pair_request(self, mac: str) -> bool: """Send a pair request to a Plus device.""" if self._network is None: raise StickError("Cannot pair when network is not initialized") - try: - await self._network.pair_plus_device(mac) - except NodeError as exc: - raise NodeError(f"{exc}") from exc - return True + + return self._network.pair_plus_device(mac) @raise_not_connected @raise_not_initialized diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 8c9b0e695..a6ff22835 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -157,7 +157,7 @@ def registry(self) -> list[str]: # endregion - async def pair_plus_device(self, mac: str) -> None: + async def pair_plus_device(self, mac: str) -> bool: """Pair Plus-device to Plugwise Stick. According to https://roheve.wordpress.com/author/roheve/page/2/ @@ -204,6 +204,8 @@ async def pair_plus_device(self, mac: str) -> None: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") + return True + async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From c4d62c0345f77e180fec424fb3a22bd1e2dde8c9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:32:20 +0100 Subject: [PATCH 015/123] Add missing await --- plugwise_usb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 6bac7da7d..f61a87fe9 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -285,7 +285,7 @@ async def plus_pair_request(self, mac: str) -> bool: if self._network is None: raise StickError("Cannot pair when network is not initialized") - return self._network.pair_plus_device(mac) + return await self._network.pair_plus_device(mac) @raise_not_connected @raise_not_initialized From b95eab44a9cc4a6e811143a7bb2c5b0db4948269 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 09:40:48 +0100 Subject: [PATCH 016/123] Add 0003 response to docstring --- plugwise_usb/network/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index a6ff22835..aedc486dd 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -162,10 +162,10 @@ async def pair_plus_device(self, mac: str) -> bool: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002: StickNetworkInfoRequest - StickNetworkInfoResponse - 000A - 0011: StickInitRequest - StickInitResponse - 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse - The Plus-device will then send a NodeRejoinResponse (0061). + 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), + 000A - 0011: StickInitRequest - StickInitResponse, + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, + the Plus-device will then send a NodeRejoinResponse (0061). Todo(?): Does this need repeating until pairing is successful? """ From 7ec42a6149d0bb146598b91775cbe5b6070e2978 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:18:14 +0100 Subject: [PATCH 017/123] Start adding pairing test --- tests/stick_pair_data.py | 60 ++++++ tests/test_pairing.py | 396 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 tests/stick_pair_data.py create mode 100644 tests/test_pairing.py diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py new file mode 100644 index 000000000..186601fec --- /dev/null +++ b/tests/stick_pair_data.py @@ -0,0 +1,60 @@ +"""Plus-device pairing test data.""" + +RESPONSE_MESSAGES = { + b"\x05\x05\x03\x030001CAAB\r\n": ( + "Stick network info request", + b"000000C1", # Success ack + b"0002" # response msg_id + + b"0F" # channel + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"1606" + + b"01", + b"0003" # response msg_id + + b"00CE", # ? + ), + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"01" # network_is_online + + b"0098765432101234" # circle_plus_mac + + b"4321" # network_id + + b"FF", # unknown2 + ), + b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + "Pair request of plus-device 0098765432101234", + b"000000C1", # Success ack + b"0005" # response msg_id + + b"00" # existing + + b"01", # allowed + ), + b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( + "Node Info of stick 0123456789012345", + b"000000C1", # Success ack + b"0024" # msg_id + + b"0123456789012345" # mac + + b"00000000" # datetime + + b"00000000" # log address 0 + + b"00" # relay + + b"80" # hz + + b"653907008512" # hw_ver + + b"4E0843A9" # fw_ver + + b"00", # node_type (Stick) + ), +} + +FIRST_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"00", # network_is_offline + ), +} \ No newline at end of file diff --git a/tests/test_pairing.py b/tests/test_pairing.py new file mode 100644 index 000000000..96e48e83a --- /dev/null +++ b/tests/test_pairing.py @@ -0,0 +1,396 @@ +"""Test pairing plus-device to plugwise USB Stick.""" + +import asyncio +from collections.abc import Callable, Coroutine +from datetime import UTC, datetime as dt, timedelta as td +import importlib +import logging +import random +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +import pytest + +import aiofiles # type: ignore[import-untyped] +import crcmod +from freezegun import freeze_time + +crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000) + +pw_stick = importlib.import_module("plugwise_usb") +pw_api = importlib.import_module("plugwise_usb.api") +pw_exceptions = importlib.import_module("plugwise_usb.exceptions") +pw_connection = importlib.import_module("plugwise_usb.connection") +pw_connection_manager = importlib.import_module("plugwise_usb.connection.manager") +pw_constants = importlib.import_module("plugwise_usb.constants") +pw_helpers_cache = importlib.import_module("plugwise_usb.helpers.cache") +pw_network_cache = importlib.import_module("plugwise_usb.network.cache") +pw_node_cache = importlib.import_module("plugwise_usb.nodes.helpers.cache") +pw_receiver = importlib.import_module("plugwise_usb.connection.receiver") +pw_sender = importlib.import_module("plugwise_usb.connection.sender") +pw_requests = importlib.import_module("plugwise_usb.messages.requests") +pw_responses = importlib.import_module("plugwise_usb.messages.responses") +pw_msg_properties = importlib.import_module("plugwise_usb.messages.properties") +pw_userdata = importlib.import_module("stick_test_data") +pw_node = importlib.import_module("plugwise_usb.nodes.node") +pw_circle = importlib.import_module("plugwise_usb.nodes.circle") +pw_sed = importlib.import_module("plugwise_usb.nodes.sed") +pw_scan = importlib.import_module("plugwise_usb.nodes.scan") +pw_sense = importlib.import_module("plugwise_usb.nodes.sense") +pw_switch = importlib.import_module("plugwise_usb.nodes.switch") +pw_energy_counter = importlib.import_module("plugwise_usb.nodes.helpers.counter") +pw_energy_calibration = importlib.import_module("plugwise_usb.nodes.helpers") +pw_energy_pulses = importlib.import_module("plugwise_usb.nodes.helpers.pulses") + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + + +def inc_seq_id(seq_id: bytes | None) -> bytes: + """Increment sequence id.""" + if seq_id is None: + return b"0000" + temp_int = int(seq_id, 16) + 1 + if temp_int >= 65532: + temp_int = 0 + temp_str = str(hex(temp_int)).lstrip("0x").upper() + while len(temp_str) < 4: + temp_str = "0" + temp_str + return temp_str.encode() + + +def construct_message(data: bytes, seq_id: bytes = b"0000") -> bytes: + """Construct plugwise message.""" + body = data[:4] + seq_id + data[4:] + return bytes( + pw_constants.MESSAGE_HEADER + + body + + bytes(f"{crc_fun(body):04X}", pw_constants.UTF8) + + pw_constants.MESSAGE_FOOTER + ) + + +class DummyTransport: + """Dummy transport class.""" + + protocol_data_received: Callable[[bytes], None] + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + test_data: dict[bytes, tuple[str, bytes, bytes | None]] | None = None, + ) -> None: + """Initialize dummy transport class.""" + self._loop = loop + self._msg = 0 + self._seq_id = b"1233" + self._processed: list[bytes] = [] + self._first_response = test_data + self._second_response = test_data + if test_data is None: + self._first_response = pw_userdata.RESPONSE_MESSAGES + self._second_response = pw_userdata.SECOND_RESPONSE_MESSAGES + self.random_extra_byte = 0 + self._closing = False + + def is_closing(self) -> bool: + """Close connection.""" + return self._closing + + def write(self, data: bytes) -> None: + """Write data back to system.""" + log = None + ack = None + response = None + if data in self._processed and self._second_response is not None: + log, ack, response = self._second_response.get(data, (None, None, None)) + if log is None and self._first_response is not None: + log, ack, response = self._first_response.get(data, (None, None, None)) + if log is None: + resp = pw_userdata.PARTLY_RESPONSE_MESSAGES.get( + data[:24], (None, None, None) + ) + if resp is None: + _LOGGER.debug("No msg response for %s", str(data)) + return + log, ack, response = resp + if ack is None: + _LOGGER.debug("No ack response for %s", str(data)) + return + + self._seq_id = inc_seq_id(self._seq_id) + if response and self._msg == 0: + self.message_response_at_once(ack, response, self._seq_id) + self._processed.append(data) + else: + self.message_response(ack, self._seq_id) + self._processed.append(data) + if response is None or self._closing: + return + self._loop.create_task(self._delayed_response(response, self._seq_id)) + self._msg += 1 + + async def _delayed_response(self, data: bytes, seq_id: bytes) -> None: + delay = random.uniform(0.005, 0.025) + await asyncio.sleep(delay) + self.message_response(data, seq_id) + + def message_response(self, data: bytes, seq_id: bytes) -> None: + """Handle message response.""" + self.random_extra_byte += 1 + if self.random_extra_byte > 25: + self.protocol_data_received(b"\x83") + self.random_extra_byte = 0 + self.protocol_data_received(construct_message(data, seq_id) + b"\x83") + else: + self.protocol_data_received(construct_message(data, seq_id)) + + def message_response_at_once(self, ack: bytes, data: bytes, seq_id: bytes) -> None: + """Full message.""" + self.random_extra_byte += 1 + if self.random_extra_byte > 25: + self.protocol_data_received(b"\x83") + self.random_extra_byte = 0 + self.protocol_data_received( + construct_message(ack, seq_id) + + construct_message(data, seq_id) + + b"\x83" + ) + else: + self.protocol_data_received( + construct_message(ack, seq_id) + construct_message(data, seq_id) + ) + + def close(self) -> None: + """Close connection.""" + self._closing = True + + +class MockSerial: + """Mock serial connection.""" + + def __init__( + self, custom_response: dict[bytes, tuple[str, bytes, bytes | None]] | None + ) -> None: + """Init mocked serial connection.""" + self.custom_response = custom_response + self._protocol: pw_receiver.StickReceiver | None = None # type: ignore[name-defined] + self._transport: DummyTransport | None = None + + def inject_message(self, data: bytes, seq_id: bytes) -> None: + """Inject message to be received from stick.""" + if self._transport is None: + return + self._transport.message_response(data, seq_id) + + def trigger_connection_lost(self) -> None: + """Trigger connection lost.""" + if self._protocol is None: + return + self._protocol.connection_lost() + + async def mock_connection( + self, + loop: asyncio.AbstractEventLoop, + protocol_factory: Callable[[], pw_receiver.StickReceiver], # type: ignore[name-defined] + **kwargs: dict[str, Any], + ) -> tuple[DummyTransport, pw_receiver.StickReceiver]: # type: ignore[name-defined] + """Mock connection with dummy connection.""" + self._protocol = protocol_factory() + self._transport = DummyTransport(loop, self.custom_response) + self._transport.protocol_data_received = self._protocol.data_received + loop.call_soon_threadsafe(self._protocol.connection_made, self._transport) + return self._transport, self._protocol + + +class MockOsPath: + """Mock aiofiles.path class.""" + + async def exists(self, file_or_path: str) -> bool: # noqa: PLR0911 + """Exists folder.""" + test_exists = [ + "mock_folder_that_exists", + "mock_folder_that_exists/nodetype.cache", + "mock_folder_that_exists\\nodetype.cache", + "mock_folder_that_exists/0123456789ABCDEF.cache", + "mock_folder_that_exists\\0123456789ABCDEF.cache", + "mock_folder_that_exists\\file_that_exists.ext", + ] + if file_or_path in test_exists: + return True + return file_or_path == "mock_folder_that_exists/file_that_exists.ext" + + async def mkdir(self, path: str) -> None: + """Make dir.""" + return + + +class MockStickController: + """Mock stick controller.""" + + def __init__(self) -> None: + """Initialize MockStickController.""" + self.send_response: list[pw_responses.PlugwiseResponse] = [] + + async def subscribe_to_messages( + self, + node_response_callback: Callable[ # type: ignore[name-defined] + [pw_responses.PlugwiseResponse], Coroutine[Any, Any, bool] + ], + mac: bytes | None = None, + message_ids: tuple[bytes] | None = None, + ) -> Callable[[], None]: + """Subscribe a awaitable callback to be called when a specific message is received. + + Returns function to unsubscribe. + """ + + def dummy_method() -> None: + """Fake method.""" + + return dummy_method + + def append_response(self, response) -> None: + """Add response to queue.""" + self.send_response.append(response) + + def clear_responses(self) -> None: + """Clear response queue.""" + self.send_response.clear() + + async def send( + self, + request: pw_requests.PlugwiseRequest, # type: ignore[name-defined] + suppress_node_errors=True, + ) -> pw_responses.PlugwiseResponse | None: # type: ignore[name-defined] + """Submit request to queue and return response.""" + if self.send_response: + return self.send_response.pop(0) + return None + + +aiofiles.threadpool.wrap.register(MagicMock)( + lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) # pylint: disable=unnecessary-lambda +) + + +class TestStick: + """Test USB Stick.""" + + test_node_awake: asyncio.Future[str] + test_node_loaded: asyncio.Future[str] + test_node_join: asyncio.Future[str] + test_connected: asyncio.Future[bool] + test_disconnected: asyncio.Future[bool] + test_relay_state_on: asyncio.Future[bool] + test_relay_state_off: asyncio.Future[bool] + test_motion_on: asyncio.Future[bool] + test_motion_off: asyncio.Future[bool] + test_init_relay_state_off: asyncio.Future[bool] + test_init_relay_state_on: asyncio.Future[bool] + + async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> None: # type: ignore[name-defined] + """Callable dummy routine.""" + return + + @pytest.mark.asyncio + async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test connecting to stick.""" + monkeypatch.setattr( + pw_connection_manager, + "create_serial_connection", + MockSerial(None).mock_connection, + ) + stick = pw_stick.Stick(port="test_port", cache_enabled=False) + + unsub_connect = stick.subscribe_to_stick_events( + stick_event_callback=self.connected, + events=(pw_api.StickEvent.CONNECTED,), + ) + self.test_connected = asyncio.Future() + await stick.connect("test_port") + assert await self.test_connected + await stick.initialize() + assert stick.mac_stick == "0123456789012345" + assert stick.name == "Stick 12345" + assert stick.mac_coordinator == "0098765432101234" + assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) + assert stick.hardware == "070085" + assert not stick.network_discovered + assert stick.network_state + assert stick.network_id == 17185 + unsub_connect() + await stick.disconnect() + assert not stick.network_state + with pytest.raises(pw_exceptions.StickError): + stick.mac_stick + + @pytest.mark.asyncio + async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Testing Stick init without paired Circle.""" + mock_serial = MockSerial( + { + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # response msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"00", # network_is_offline + ), + } + ) + monkeypatch.setattr( + pw_connection_manager, + "create_serial_connection", + mock_serial.mock_connection, + ) + monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) + monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) + stick = pw_stick.Stick(port="test_port", cache_enabled=False) + await stick.connect() + with pytest.raises(pw_exceptions.StickError): + await stick.initialize() + await stick.disconnect() + +# async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] +# """Handle join event callback.""" +# if event == pw_api.NodeEvent.JOIN: +# self.test_node_join.set_result(mac) +# else: +# self.test_node_join.set_exception( +# BaseException( +# f"Invalid {event} event, expected " + f"{pw_api.NodeEvent.JOIN}" +# ) +# ) + + # @pytest.mark.asyncio + # async def test_stick_node_join_subscription( + # self, monkeypatch: pytest.MonkeyPatch + # ) -> None: + # """Testing "new_node" subscription.""" + # mock_serial = MockSerial(None) + # monkeypatch.setattr( + # pw_connection_manager, + # "create_serial_connection", + # mock_serial.mock_connection, + # ) + # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) + # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) + # stick = pw_stick.Stick("test_port", cache_enabled=False) + # await stick.connect() + # await stick.initialize() + # await stick.discover_nodes(load=False) + + # self.test_node_join = asyncio.Future() + # unusb_join = stick.subscribe_to_node_events( + # node_event_callback=self.node_join, + # events=(pw_api.NodeEvent.JOIN,), + # ) + + ## Inject NodeJoinAvailableResponse + # mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! + # mac_join_node = await self.test_node_join + # assert mac_join_node == "9999999999999999" + # unusb_join() + # await stick.disconnect() From f392df37024dc9d348a0c09460ba60df9cca778d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:51:31 +0100 Subject: [PATCH 018/123] Add missing connected() --- tests/test_pairing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 96e48e83a..edbc12466 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -289,6 +289,13 @@ class TestStick: test_init_relay_state_off: asyncio.Future[bool] test_init_relay_state_on: asyncio.Future[bool] + async def connected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined] + """Set connected state helper.""" + if event is pw_api.StickEvent.CONNECTED: + self.test_connected.set_result(True) + else: + self.test_connected.set_exception(BaseException("Incorrect event")) + async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> None: # type: ignore[name-defined] """Callable dummy routine.""" return From a4d36a41628f3f8ee2ac9f2f28c4dc7ee5183b6b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 10:58:46 +0100 Subject: [PATCH 019/123] Add pairing-test --- tests/test_pairing.py | 116 +++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index edbc12466..233841bf7 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -300,64 +300,84 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No """Callable dummy routine.""" return +# @pytest.mark.asyncio +# async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: +# """Test connecting to stick.""" +# monkeypatch.setattr( +# pw_connection_manager, +# "create_serial_connection", +# MockSerial(None).mock_connection, +# ) +# stick = pw_stick.Stick(port="test_port", cache_enabled=False) + +# unsub_connect = stick.subscribe_to_stick_events( +# stick_event_callback=self.connected, +# events=(pw_api.StickEvent.CONNECTED,), +# ) +# self.test_connected = asyncio.Future() +# await stick.connect("test_port") +# assert await self.test_connected +# await stick.initialize() +# assert stick.mac_stick == "0123456789012345" +# assert stick.name == "Stick 12345" +# assert stick.mac_coordinator == "0098765432101234" +# assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) +# assert stick.hardware == "070085" +# assert not stick.network_discovered +# assert stick.network_state +# assert stick.network_id == 17185 +# unsub_connect() +# await stick.disconnect() +# assert not stick.network_state +# with pytest.raises(pw_exceptions.StickError): +# stick.mac_stick + +# @pytest.mark.asyncio +# async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: +# """Testing Stick init without paired Circle.""" +# mock_serial = MockSerial( +# { +# b"\x05\x05\x03\x03000AB43C\r\n": ( +# "STICK INIT", +# b"000000C1", # Success ack +# b"0011" # response msg_id +# + b"0123456789012345" # stick mac +# + b"00" # unknown1 +# + b"00", # network_is_offline +# ), +# } +# ) +# monkeypatch.setattr( +# pw_connection_manager, +# "create_serial_connection", +# mock_serial.mock_connection, +# ) +# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) +# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) +# stick = pw_stick.Stick(port="test_port", cache_enabled=False) +# await stick.connect() +# with pytest.raises(pw_exceptions.StickError): +# await stick.initialize() +# await stick.disconnect() + @pytest.mark.asyncio - async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test connecting to stick.""" + async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test pairing a plus-device.""" + mock_serial = MockSerial(None) monkeypatch.setattr( pw_connection_manager, "create_serial_connection", MockSerial(None).mock_connection, ) + monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) + monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) - - unsub_connect = stick.subscribe_to_stick_events( - stick_event_callback=self.connected, - events=(pw_api.StickEvent.CONNECTED,), - ) - self.test_connected = asyncio.Future() await stick.connect("test_port") - assert await self.test_connected await stick.initialize() - assert stick.mac_stick == "0123456789012345" - assert stick.name == "Stick 12345" - assert stick.mac_coordinator == "0098765432101234" - assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) - assert stick.hardware == "070085" - assert not stick.network_discovered - assert stick.network_state - assert stick.network_id == 17185 - unsub_connect() - await stick.disconnect() - assert not stick.network_state - with pytest.raises(pw_exceptions.StickError): - stick.mac_stick + + # Inject StickNetworkInfoRequest to trigger a pairing + mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! - @pytest.mark.asyncio - async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Testing Stick init without paired Circle.""" - mock_serial = MockSerial( - { - b"\x05\x05\x03\x03000AB43C\r\n": ( - "STICK INIT", - b"000000C1", # Success ack - b"0011" # response msg_id - + b"0123456789012345" # stick mac - + b"00" # unknown1 - + b"00", # network_is_offline - ), - } - ) - monkeypatch.setattr( - pw_connection_manager, - "create_serial_connection", - mock_serial.mock_connection, - ) - monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) - monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) - stick = pw_stick.Stick(port="test_port", cache_enabled=False) - await stick.connect() - with pytest.raises(pw_exceptions.StickError): - await stick.initialize() await stick.disconnect() # async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] From f628158387ce15975d60f483f5177e0c0601f922 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:09:13 +0100 Subject: [PATCH 020/123] Link to stick_pair_data --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 233841bf7..2eb76ac66 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -31,7 +31,7 @@ pw_requests = importlib.import_module("plugwise_usb.messages.requests") pw_responses = importlib.import_module("plugwise_usb.messages.responses") pw_msg_properties = importlib.import_module("plugwise_usb.messages.properties") -pw_userdata = importlib.import_module("stick_test_data") +pw_userdata = importlib.import_module("stick_pair_data") pw_node = importlib.import_module("plugwise_usb.nodes.node") pw_circle = importlib.import_module("plugwise_usb.nodes.circle") pw_sed = importlib.import_module("plugwise_usb.nodes.sed") From 92b60f5ffdf1c2fe6e0f9206b198f6fd8438696f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:10:41 +0100 Subject: [PATCH 021/123] Fix stick_pair_data --- tests/stick_pair_data.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 186601fec..999822d24 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -48,13 +48,14 @@ ), } -FIRST_RESPONSE_MESSAGES = { - b"\x05\x05\x03\x03000AB43C\r\n": ( - "STICK INIT", +SECOND_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x03000D55555555555555555E46\r\n": ( + "ping reply for 5555555555555555", b"000000C1", # Success ack - b"0011" # msg_id - + b"0123456789012345" # stick mac - + b"00" # unknown1 - + b"00", # network_is_offline - ), -} \ No newline at end of file + b"000E" + + b"5555555555555555" # mac + + b"44" # rssi in + + b"33" # rssi out + + b"0055", # roundtrip + ) +} From 9afa86ad44e2635e993fb18b7d7a922d9f3e9a91 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:12:44 +0100 Subject: [PATCH 022/123] Set network to offline --- tests/stick_pair_data.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 999822d24..d1db76ccd 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -21,10 +21,10 @@ b"0011" # msg_id + b"0123456789012345" # stick mac + b"00" # unknown1 - + b"01" # network_is_online - + b"0098765432101234" # circle_plus_mac - + b"4321" # network_id - + b"FF", # unknown2 + + b"00", # network_is_offline + # + b"0098765432101234" # circle_plus_mac + # + b"4321" # network_id + # + b"FF", # unknown2 ), b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", From 676857272208104306125fa32b998b4cc926a260 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:26:33 +0100 Subject: [PATCH 023/123] Try --- plugwise_usb/messages/responses.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 7be3cf599..aa9a29c44 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -434,16 +434,21 @@ def __init__(self) -> None: super().__init__(b"0011") self._unknown1 = Int(0, length=2) self._network_online = Int(0, length=2) - self._mac_nc = String(None, length=16) - self._network_id = Int(0, 4, False) - self._unknown2 = Int(0, length=2) self._params += [ self._unknown1, self._network_online, - self._mac_nc, - self._network_id, - self._unknown2, - ] + ] + if self._network_online == 1: + self._mac_nc = String(None, length=16) + self._network_id = Int(0, 4, False) + self._unknown2 = Int(0, length=2) + self._params += [ + self._unknown1, + self._network_online, + self._mac_nc, + self._network_id, + self._unknown2, + ] @property def mac_network_controller(self) -> str: From 226ccceb26c8634a090c45b5f9e45d898968bed5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:35:33 +0100 Subject: [PATCH 024/123] Try 2 --- plugwise_usb/messages/responses.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index aa9a29c44..fd9adf378 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -438,17 +438,17 @@ def __init__(self) -> None: self._unknown1, self._network_online, ] - if self._network_online == 1: - self._mac_nc = String(None, length=16) - self._network_id = Int(0, 4, False) - self._unknown2 = Int(0, length=2) - self._params += [ - self._unknown1, - self._network_online, - self._mac_nc, - self._network_id, - self._unknown2, - ] +# if self._network_online == 1: +# self._mac_nc = String(None, length=16) +# self._network_id = Int(0, 4, False) +# self._unknown2 = Int(0, length=2) +# self._params += [ +# self._unknown1, +# self._network_online, +# self._mac_nc, +# self._network_id, +# self._unknown2, +# ] @property def mac_network_controller(self) -> str: From dfa345064bd7a7ab18126d8f63eb7531f5d9c44a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 11:41:24 +0100 Subject: [PATCH 025/123] Try 3 --- plugwise_usb/messages/responses.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index fd9adf378..985b64914 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -437,7 +437,9 @@ def __init__(self) -> None: self._params += [ self._unknown1, self._network_online, - ] + ] + self._mac_nc = None + self._network_id = None # if self._network_online == 1: # self._mac_nc = String(None, length=16) # self._network_id = Int(0, 4, False) @@ -451,14 +453,18 @@ def __init__(self) -> None: # ] @property - def mac_network_controller(self) -> str: + def mac_network_controller(self) -> str | None: """Return the mac of the network controller (Circle+).""" # Replace first 2 characters by 00 for mac of circle+ node + if self._mac_nc is None: + return None return "00" + self._mac_nc.value[2:] @property - def network_id(self) -> int: + def network_id(self) -> int | None: """Return network ID.""" + if self._network_id is None: + return None return self._network_id.value @property From 777121d41d66004f0c37c9eb27a942f07196de25 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:16:16 +0100 Subject: [PATCH 026/123] Add StickInitShortResponse --- plugwise_usb/messages/responses.py | 75 +++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 985b64914..c42a68d70 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -417,8 +417,45 @@ def __init__(self, timestamp: datetime | None = None) -> None: self._params += [self.image_timestamp] +class StickInitShortResponse(PlugwiseResponse): + """Returns the configuration and status of the USB-Stick - no network. + + Supported protocols : 1.0, 2.0 + Response to request : StickInitRequest + """ + + def __init__(self) -> None: + """Initialize StickInitShortResponse message object.""" + super().__init__(b"0011") + self._unknown1 = Int(0, length=2) + self._network_online = Int(0, length=2) + self._params += [ + self._unknown1, + self._network_online, + ] + + @property + def mac_network_controller(self) -> str | None: + """Return the mac of the network controller (Circle+).""" + return None + + @property + def network_id(self) -> int | None: + """Return network ID.""" + return None + + @property + def network_online(self) -> bool: + """Return state of network.""" + return self._network_online.value == 1 + + def __repr__(self) -> str: + """Convert request into writable str.""" + return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" + + class StickInitResponse(PlugwiseResponse): - """Returns the configuration and status of the USB-Stick. + """Returns the configuration and status of the USB-Stick - network online. Optional: - circle_plus_mac @@ -434,37 +471,26 @@ def __init__(self) -> None: super().__init__(b"0011") self._unknown1 = Int(0, length=2) self._network_online = Int(0, length=2) + self._mac_nc = String(None, length=16) + self._network_id = Int(0, 4, False) + self._unknown2 = Int(0, length=2) self._params += [ self._unknown1, self._network_online, + self._mac_nc, + self._network_id, + self._unknown2, ] - self._mac_nc = None - self._network_id = None -# if self._network_online == 1: -# self._mac_nc = String(None, length=16) -# self._network_id = Int(0, 4, False) -# self._unknown2 = Int(0, length=2) -# self._params += [ -# self._unknown1, -# self._network_online, -# self._mac_nc, -# self._network_id, -# self._unknown2, -# ] @property - def mac_network_controller(self) -> str | None: + def mac_network_controller(self) -> str: """Return the mac of the network controller (Circle+).""" # Replace first 2 characters by 00 for mac of circle+ node - if self._mac_nc is None: - return None return "00" + self._mac_nc.value[2:] @property - def network_id(self) -> int | None: + def network_id(self) -> int: """Return network ID.""" - if self._network_id is None: - return None return self._network_id.value @property @@ -1011,8 +1037,15 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 return NodePingResponse() if identifier == b"0010": return NodeImageValidationResponse() + + # 0011 has two formats if identifier == b"0011": - return StickInitResponse() + if length == 20: + return StickInitShortResponse() + if length == 42: + return StickInitResponse() + return None + if identifier == b"0013": return CirclePowerUsageResponse() if identifier == b"0015": From c1271e5741aeccd3a3bb73fd84889894252e4f2d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:28:26 +0100 Subject: [PATCH 027/123] Remove commented-out in response --- tests/stick_pair_data.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index d1db76ccd..d1018f2a1 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -22,9 +22,6 @@ + b"0123456789012345" # stick mac + b"00" # unknown1 + b"00", # network_is_offline - # + b"0098765432101234" # circle_plus_mac - # + b"4321" # network_id - # + b"FF", # unknown2 ), b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", From 8cfff682ba5977bfdcd5aa48e51eb6d77494ff9a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:34:02 +0100 Subject: [PATCH 028/123] Update length --- plugwise_usb/messages/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index c42a68d70..abbbe59d4 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1040,9 +1040,9 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 # 0011 has two formats if identifier == b"0011": - if length == 20: + if length == 36: return StickInitShortResponse() - if length == 42: + if length == 60: return StickInitResponse() return None From 1b8c38100cc94cb01943a2d497f101ccdb7f7bf7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:41:44 +0100 Subject: [PATCH 029/123] Adapt StickInitRequest send() --- plugwise_usb/messages/requests.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 26f5a37ae..12d684fda 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 ae9208b29d07c3d3cdad6601f9caf632485d5200 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:49:23 +0100 Subject: [PATCH 030/123] Update length StickInitResponse --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index abbbe59d4..800c80ae7 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -1042,7 +1042,7 @@ def get_message_object( # noqa: C901 PLR0911 PLR0912 if identifier == b"0011": if length == 36: return StickInitShortResponse() - if length == 60: + if length == 58: return StickInitResponse() return None From 97ab5003853a30cc9eeb75fdf27be6ed0c94f51e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:57:19 +0100 Subject: [PATCH 031/123] Clean up --- tests/stick_pair_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index d1018f2a1..195846adb 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -42,7 +42,7 @@ + b"653907008512" # hw_ver + b"4E0843A9" # fw_ver + b"00", # node_type (Stick) - ), + ), } SECOND_RESPONSE_MESSAGES = { From 315e26a61ba3550f4018580033fff6f54daaa404 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 12:59:30 +0100 Subject: [PATCH 032/123] Full test-output - test_pairing --- scripts/tests_and_coverage.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index e3b7ee971..5c0ad8ac7 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,7 +56,8 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/ + # PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || + PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 85a103e19cf5d473115741aa3c5416b8cfdab9c1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:11:30 +0100 Subject: [PATCH 033/123] Allow init to fail --- tests/test_pairing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 2eb76ac66..502f40bc3 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -373,7 +373,8 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect("test_port") - await stick.initialize() + with pytest.raises(pw_exceptions.StickError): + await stick.initialize() # Inject StickNetworkInfoRequest to trigger a pairing mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! From 68b3333fd18e20e641a717a7817c5fd8f5b18b39 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:13:36 +0100 Subject: [PATCH 034/123] Add sleep --- tests/test_pairing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 502f40bc3..8a202e274 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -376,6 +376,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: with pytest.raises(pw_exceptions.StickError): await stick.initialize() + await asyncio.sleep(5) # Inject StickNetworkInfoRequest to trigger a pairing mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! From 81ae16acb3ee0b5d1c2a306814f01305de41e60a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:42:31 +0100 Subject: [PATCH 035/123] Call pair_plus_request() --- tests/test_pairing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 8a202e274..eb975b0e2 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -377,8 +377,8 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.initialize() await asyncio.sleep(5) - # Inject StickNetworkInfoRequest to trigger a pairing - mock_serial.inject_message(b"0001", b"1253") # @bouwew: seq_id is not FFFC! + await stick.plus_pair_request("0123456789012345") + await asyncio.sleep(5) await stick.disconnect() From d2467faa0dfe7d2334b4e469844e4e0e272cf0e2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 13:46:12 +0100 Subject: [PATCH 036/123] Connected and initialized is not required --- plugwise_usb/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index f61a87fe9..2125368ca 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -278,13 +278,8 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() - @raise_not_connected - @raise_not_initialized async def plus_pair_request(self, mac: str) -> bool: """Send a pair request to a Plus device.""" - if self._network is None: - raise StickError("Cannot pair when network is not initialized") - return await self._network.pair_plus_device(mac) @raise_not_connected From 7683d781d2927ce3fede737856de9668ca457233 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sun, 8 Feb 2026 12:49:30 +0000 Subject: [PATCH 037/123] fixup: pair-plus Python code fixed using Ruff --- plugwise_usb/messages/requests.py | 4 +- tests/test_pairing.py | 181 +++++++++++++++--------------- 2 files changed, 94 insertions(+), 91 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 12d684fda..16b319635 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 74f986e30593d6f283027400f52c46388ba41c44 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:33:41 +0100 Subject: [PATCH 038/123] There can only be one response --- tests/stick_pair_data.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 195846adb..654ad2591 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -10,10 +10,8 @@ + b"0698765432101234" # 06 + plus-device mac + b"FFFFFFFFFFFFFFFF" + b"0698765432101234" # 06 + plus-device mac - + b"1606" - + b"01", - b"0003" # response msg_id - + b"00CE", # ? + + b"1606" # pan_id + + b"01", # index ), b"\x05\x05\x03\x03000AB43C\r\n": ( "STICK INIT", From b91c32425af1c492c445edbe03bc502c74227796 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:38:21 +0100 Subject: [PATCH 039/123] Use inheritance for StickInitResponse --- plugwise_usb/messages/responses.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 800c80ae7..9f620216e 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -454,7 +454,7 @@ def __repr__(self) -> str: return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" -class StickInitResponse(PlugwiseResponse): +class StickInitResponse(StickInitShortResponse): """Returns the configuration and status of the USB-Stick - network online. Optional: @@ -468,15 +468,11 @@ class StickInitResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize StickInitResponse message object.""" - super().__init__(b"0011") - self._unknown1 = Int(0, length=2) - self._network_online = Int(0, length=2) + super().__init__() self._mac_nc = String(None, length=16) self._network_id = Int(0, 4, False) self._unknown2 = Int(0, length=2) self._params += [ - self._unknown1, - self._network_online, self._mac_nc, self._network_id, self._unknown2, @@ -493,15 +489,6 @@ def network_id(self) -> int: """Return network ID.""" return self._network_id.value - @property - def network_online(self) -> bool: - """Return state of network.""" - return self._network_online.value == 1 - - def __repr__(self) -> str: - """Convert request into writable str.""" - return f"{super().__repr__()[:-1]}, network_controller={self.mac_network_controller}, network_online={self.network_online})" - class CirclePowerUsageResponse(PlugwiseResponse): """Returns power usage as impulse counters for several different time frames. From 21299bdefec48d2df61276ef8be1cc0d9b379106 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 19:52:34 +0100 Subject: [PATCH 040/123] Move pair_plus_device() to connection --- plugwise_usb/__init__.py | 8 ++--- plugwise_usb/connection/__init__.py | 53 ++++++++++++++++++++++++++++- plugwise_usb/network/__init__.py | 51 --------------------------- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 2125368ca..fa4de8144 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -266,6 +266,10 @@ async def connect(self, port: str | None = None) -> None: self._port, ) + async def plus_pair_request(self, mac: str) -> bool: + """Send a pair request to a Plus device.""" + return await self._controller.pair_plus_device(mac) + @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" @@ -278,10 +282,6 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() - async def plus_pair_request(self, mac: str) -> bool: - """Send a pair request to a Plus device.""" - return await self._network.pair_plus_device(mac) - @raise_not_connected @raise_not_initialized async def start_network(self) -> None: diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index ff17f1400..fd19b4e8f 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -9,12 +9,14 @@ from ..api import StickEvent from ..constants import UTF8 from ..exceptions import NodeError, StickError -from ..helpers.util import version_to_model +from ..helpers.util import validate_mac, version_to_model from ..messages.requests import ( + CirclePlusConnectRequest, NodeInfoRequest, NodePingRequest, PlugwiseRequest, StickInitRequest, + StickNetworkInfoRequest, ) from ..messages.responses import ( NodeInfoResponse, @@ -202,6 +204,55 @@ async def initialize_stick(self) -> None: if not self._network_online: raise StickError("Zigbee network connection to Circle+ is down.") + async def pair_plus_device(self, mac: str) -> bool: + """Pair Plus-device to Plugwise Stick. + + According to https://roheve.wordpress.com/author/roheve/page/2/ + The pairing process should look like: + 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), + 000A - 0011: StickInitRequest - StickInitResponse, + 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, + the Plus-device will then send a NodeRejoinResponse (0061). + + Todo(?): Does this need repeating until pairing is successful? + """ + _LOGGER.debug("Pair Plus-device with mac: %s", mac) + if not validate_mac(mac): + raise NodeError(f"Pairing failed: MAC {mac} invalid") + + # Collect network info + try: + request = StickNetworkInfoRequest(self.send, None) + info_response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if info_response is None: + raise NodeError( + "Pairing failed, StickNetworkInfoResponse is None" + ) from None + + # Init Stick + try: + await self.initialize_stick() + except StickError as exc: + raise NodeError( + f"Pairing failed, failed to initialize Stick: {exc}" + ) from exc + + try: + request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if response is None: + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") + + return True + async def get_node_details( self, mac: str, ping_first: bool ) -> tuple[NodeInfoResponse | None, NodePingResponse | None]: diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index aedc486dd..50834a751 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -22,9 +22,7 @@ from ..helpers.util import validate_mac from ..messages.requests import ( CircleMeasureIntervalRequest, - CirclePlusConnectRequest, NodePingRequest, - StickNetworkInfoRequest, ) from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, @@ -157,55 +155,6 @@ def registry(self) -> list[str]: # endregion - async def pair_plus_device(self, mac: str) -> bool: - """Pair Plus-device to Plugwise Stick. - - According to https://roheve.wordpress.com/author/roheve/page/2/ - The pairing process should look like: - 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), - 000A - 0011: StickInitRequest - StickInitResponse, - 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, - the Plus-device will then send a NodeRejoinResponse (0061). - - Todo(?): Does this need repeating until pairing is successful? - """ - _LOGGER.debug("Pair Plus-device with mac: %s", mac) - if not validate_mac(mac): - raise NodeError(f"Pairing failed: MAC {mac} invalid") - - # Collect network info - try: - request = StickNetworkInfoRequest(self._controller.send, None) - info_response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if info_response is None: - raise NodeError( - "Pairing failed, StickNetworkInfoResponse is None" - ) from None - - # Init Stick - try: - await self._controller.initialize_stick() - except StickError as exc: - raise NodeError( - f"Pairing failed, failed to initialize Stick: {exc}" - ) from exc - - try: - request = CirclePlusConnectRequest(self._controller.send, bytes(mac, UTF8)) - response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if response is None: - raise NodeError( - "Pairing failed, CirclePlusConnectResponse is None" - ) from None - if response.allowed.value != 1: - raise NodeError("Pairing failed, not allowed") - - return True - async def register_node(self, mac: str) -> bool: """Register node to Plugwise network.""" try: From e066b4b308ee6e48d9df9b55d03e62f977761bcf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:03:58 +0100 Subject: [PATCH 041/123] Correct plus-mac --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 33f09339c..bb535be12 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -377,7 +377,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.initialize() await asyncio.sleep(5) - await stick.plus_pair_request("0123456789012345") + await stick.plus_pair_request("0098765432101234") await asyncio.sleep(5) await stick.disconnect() From 2a62536fbeac243daba0f41c2038c2b5b3f4e74a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Feb 2026 20:08:47 +0100 Subject: [PATCH 042/123] Add stick-mac to 0002 response --- tests/stick_pair_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/stick_pair_data.py b/tests/stick_pair_data.py index 654ad2591..d6d4ff947 100644 --- a/tests/stick_pair_data.py +++ b/tests/stick_pair_data.py @@ -5,6 +5,7 @@ "Stick network info request", b"000000C1", # Success ack b"0002" # response msg_id + + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" + b"0698765432101234" # 06 + plus-device mac From 5fbd81a52abdec1fbf647ce186a4337d2934ef10 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:35:55 +0100 Subject: [PATCH 043/123] Move RESPONSE_MESSAGES --- tests/test_pairing.py | 110 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index bb535be12..64d900104 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -31,7 +31,6 @@ pw_requests = importlib.import_module("plugwise_usb.messages.requests") pw_responses = importlib.import_module("plugwise_usb.messages.responses") pw_msg_properties = importlib.import_module("plugwise_usb.messages.properties") -pw_userdata = importlib.import_module("stick_pair_data") pw_node = importlib.import_module("plugwise_usb.nodes.node") pw_circle = importlib.import_module("plugwise_usb.nodes.circle") pw_sed = importlib.import_module("plugwise_usb.nodes.sed") @@ -45,6 +44,61 @@ _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) +RESPONSE_MESSAGES = { + b"\x05\x05\x03\x030001CAAB\r\n": ( + "Stick network info request", + b"000000C1", # Success ack + b"0002" # response msg_id + + b"0123456789012345" # stick-mac + + b"0F" # channel + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"FFFFFFFFFFFFFFFF" + + b"0698765432101234" # 06 + plus-device mac + + b"1606" # pan_id + + b"01", # index + ), + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"00", # network_is_offline + ), + b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + "Pair request of plus-device 0098765432101234", + b"000000C1", # Success ack + b"0005" # response msg_id + + b"00" # existing + + b"01", # allowed + ), + b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( + "Node Info of stick 0123456789012345", + b"000000C1", # Success ack + b"0024" # msg_id + + b"0123456789012345" # mac + + b"00000000" # datetime + + b"00000000" # log address 0 + + b"00" # relay + + b"80" # hz + + b"653907008512" # hw_ver + + b"4E0843A9" # fw_ver + + b"00", # node_type (Stick) + ), +} + +SECOND_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x03000D55555555555555555E46\r\n": ( + "ping reply for 5555555555555555", + b"000000C1", # Success ack + b"000E" + + b"5555555555555555" # mac + + b"44" # rssi in + + b"33" # rssi out + + b"0055", # roundtrip + ) +} def inc_seq_id(seq_id: bytes | None) -> bytes: """Increment sequence id.""" @@ -88,8 +142,8 @@ def __init__( self._first_response = test_data self._second_response = test_data if test_data is None: - self._first_response = pw_userdata.RESPONSE_MESSAGES - self._second_response = pw_userdata.SECOND_RESPONSE_MESSAGES + self._first_response = RESPONSE_MESSAGES + self._second_response = SECOND_RESPONSE_MESSAGES self.random_extra_byte = 0 self._closing = False @@ -107,7 +161,7 @@ def write(self, data: bytes) -> None: if log is None and self._first_response is not None: log, ack, response = self._first_response.get(data, (None, None, None)) if log is None: - resp = pw_userdata.PARTLY_RESPONSE_MESSAGES.get( + resp = PARTLY_RESPONSE_MESSAGES.get( data[:24], (None, None, None) ) if resp is None: @@ -360,6 +414,54 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No # await stick.initialize() # await stick.disconnect() + RESPONSE_MESSAGES = { + b"\x05\x05\x03\x030001CAAB\r\n": ( + "Stick network info request", + b"000000C1", # Success ack + b"0002" # response msg_id + + b"0123456789012345" # stick-mac + + b"0F" # channel + + b"FFFFFFFFFFFFFFFF" + + b"FF98765432101234" # 06 + plus-device mac + + b"FFFFFFFFFFFFFFFF" + + b"FF98765432101234" # 06 + plus-device mac + + b"04FF" # pan_id + + b"01", # index + ), + b"\x05\x05\x03\x03000AB43C\r\n": ( + "STICK INIT", + b"000000C1", # Success ack + b"0011" # msg_id + + b"0123456789012345" # stick mac + + b"00" # unknown1 + + b"01" # network_is_online + + b"FF98765432101234" + + b"04FF" + + b"FF", + ), + b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( + "Pair request of plus-device 0098765432101234", + b"000000C1", # Success ack + b"0005" # response msg_id + + b"00" # existing + + b"01", # allowed + ), + b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( + "Node Info of stick 0123456789012345", + b"000000C1", # Success ack + b"0024" # msg_id + + b"0123456789012345" # mac + + b"00000000" # datetime + + b"00000000" # log address 0 + + b"00" # relay + + b"80" # hz + + b"653907008512" # hw_ver + + b"4E0843A9" # fw_ver + + b"00", # node_type (Stick) + ), + } + + @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" From afedc85bf126b42028cebf1d82ca36aa48386be7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:46:23 +0100 Subject: [PATCH 044/123] Don't test network down first --- tests/test_pairing.py | 67 +++++++------------------------------------ 1 file changed, 10 insertions(+), 57 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 64d900104..61504f148 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -52,10 +52,10 @@ + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" - + b"0698765432101234" # 06 + plus-device mac + + b"FF98765432101234" # 06 + plus-device mac + b"FFFFFFFFFFFFFFFF" - + b"0698765432101234" # 06 + plus-device mac - + b"1606" # pan_id + + b"FF98765432101234" # 06 + plus-device mac + + b"04FF" # pan_id + b"01", # index ), b"\x05\x05\x03\x03000AB43C\r\n": ( @@ -64,7 +64,10 @@ b"0011" # msg_id + b"0123456789012345" # stick mac + b"00" # unknown1 - + b"00", # network_is_offline + + b"01" # network_is_online + + b"FF98765432101234" + + b"04FF" + + b"FF", ), b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( "Pair request of plus-device 0098765432101234", @@ -414,54 +417,6 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No # await stick.initialize() # await stick.disconnect() - RESPONSE_MESSAGES = { - b"\x05\x05\x03\x030001CAAB\r\n": ( - "Stick network info request", - b"000000C1", # Success ack - b"0002" # response msg_id - + b"0123456789012345" # stick-mac - + b"0F" # channel - + b"FFFFFFFFFFFFFFFF" - + b"FF98765432101234" # 06 + plus-device mac - + b"FFFFFFFFFFFFFFFF" - + b"FF98765432101234" # 06 + plus-device mac - + b"04FF" # pan_id - + b"01", # index - ), - b"\x05\x05\x03\x03000AB43C\r\n": ( - "STICK INIT", - b"000000C1", # Success ack - b"0011" # msg_id - + b"0123456789012345" # stick mac - + b"00" # unknown1 - + b"01" # network_is_online - + b"FF98765432101234" - + b"04FF" - + b"FF", - ), - b"\x05\x05\x03\x0300040000000000000000000098765432101234\r\n": ( - "Pair request of plus-device 0098765432101234", - b"000000C1", # Success ack - b"0005" # response msg_id - + b"00" # existing - + b"01", # allowed - ), - b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( - "Node Info of stick 0123456789012345", - b"000000C1", # Success ack - b"0024" # msg_id - + b"0123456789012345" # mac - + b"00000000" # datetime - + b"00000000" # log address 0 - + b"00" # relay - + b"80" # hz - + b"653907008512" # hw_ver - + b"4E0843A9" # fw_ver - + b"00", # node_type (Stick) - ), - } - - @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" @@ -474,11 +429,9 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) - await stick.connect("test_port") - with pytest.raises(pw_exceptions.StickError): - await stick.initialize() - - await asyncio.sleep(5) + # await stick.connect("test_port") + # with pytest.raises(pw_exceptions.StickError): + # await stick.initialize() await stick.plus_pair_request("0098765432101234") await asyncio.sleep(5) From f53b3362629bcbf598cb77a7ab94ace9203681bc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:47:25 +0100 Subject: [PATCH 045/123] Add missing import --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index fd19b4e8f..58be25c4f 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -8,7 +8,7 @@ from ..api import StickEvent from ..constants import UTF8 -from ..exceptions import NodeError, StickError +from ..exceptions import MessageError, NodeError, StickError from ..helpers.util import validate_mac, version_to_model from ..messages.requests import ( CirclePlusConnectRequest, From 8f517450680ebede94c1213a6d67f23feb1cfdf3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:49:34 +0100 Subject: [PATCH 046/123] Connect first --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 61504f148..fa2f3428c 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -429,7 +429,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) - # await stick.connect("test_port") + await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): # await stick.initialize() await stick.plus_pair_request("0098765432101234") From 10e408eb88c07e36ff5b24ead7a338bc97c707c5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 9 Feb 2026 08:52:43 +0100 Subject: [PATCH 047/123] Try --- tests/test_pairing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index fa2f3428c..97f408505 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -431,9 +431,9 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): - # await stick.initialize() - await stick.plus_pair_request("0098765432101234") - await asyncio.sleep(5) + await stick.initialize() + # await stick.plus_pair_request("0098765432101234") + # await asyncio.sleep(5) await stick.disconnect() From 699f0b15d3a98f75277349aa2d1852000609aa20 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 9 Feb 2026 07:56:13 +0000 Subject: [PATCH 048/123] fixup: pair-plus Python code fixed using Ruff --- tests/test_pairing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 97f408505..889fbe063 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -103,6 +103,7 @@ ) } + def inc_seq_id(seq_id: bytes | None) -> bytes: """Increment sequence id.""" if seq_id is None: @@ -164,9 +165,7 @@ def write(self, data: bytes) -> None: if log is None and self._first_response is not None: log, ack, response = self._first_response.get(data, (None, None, None)) if log is None: - resp = PARTLY_RESPONSE_MESSAGES.get( - data[:24], (None, None, None) - ) + resp = PARTLY_RESPONSE_MESSAGES.get(data[:24], (None, None, None)) if resp is None: _LOGGER.debug("No msg response for %s", str(data)) return From f53f1f51a6706bfd85945fbfa2e3d237b5d27ed0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:08:32 +0100 Subject: [PATCH 049/123] Try 3 --- plugwise_usb/connection/__init__.py | 26 ++++++++++++++------------ tests/test_pairing.py | 6 ++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 58be25c4f..2df82314b 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -230,6 +230,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None + _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -238,18 +239,19 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - - try: - request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) - response = await request.send() - except MessageError as exc: - raise NodeError(f"Pairing failed: {exc}") from exc - if response is None: - raise NodeError( - "Pairing failed, CirclePlusConnectResponse is None" - ) from None - if response.allowed.value != 1: - raise NodeError("Pairing failed, not allowed") + _LOGGER.debug("HOI Init done") + + #try: + # request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + # response = await request.send() + #except MessageError as exc: + # raise NodeError(f"Pairing failed: {exc}") from exc + #if response is None: + # raise NodeError( + # "Pairing failed, CirclePlusConnectResponse is None" + # ) from None + #if response.allowed.value != 1: + # raise NodeError("Pairing failed, not allowed") return True diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 889fbe063..9508b6dbd 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -431,8 +431,10 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await stick.connect("test_port") # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - # await stick.plus_pair_request("0098765432101234") - # await asyncio.sleep(5) + + await asyncio.sleep(2) + await stick.plus_pair_request("0098765432101234") + await asyncio.sleep(2) await stick.disconnect() From 01d787b4e277732c8b86e5d6005c00cd35cfea89 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:15:32 +0100 Subject: [PATCH 050/123] Try 4 --- plugwise_usb/connection/__init__.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 2df82314b..507589cb0 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -241,17 +241,19 @@ async def pair_plus_device(self, mac: str) -> bool: ) from exc _LOGGER.debug("HOI Init done") - #try: - # request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) - # response = await request.send() - #except MessageError as exc: - # raise NodeError(f"Pairing failed: {exc}") from exc - #if response is None: - # raise NodeError( - # "Pairing failed, CirclePlusConnectResponse is None" - # ) from None - #if response.allowed.value != 1: - # raise NodeError("Pairing failed, not allowed") + try: + request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) + response = await request.send() + except MessageError as exc: + raise NodeError(f"Pairing failed: {exc}") from exc + if response is None: + raise NodeError( + "Pairing failed, CirclePlusConnectResponse is None" + ) from None + if response.allowed.value != 1: + raise NodeError("Pairing failed, not allowed") + + _LOGGER.debug("HOI PlusConnectRequest done") return True From e8ea7551fdf9bccf200cd11bba8cec1386d207a0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:27:31 +0100 Subject: [PATCH 051/123] Try not allowed --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 9508b6dbd..fbe9e0750 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -74,7 +74,7 @@ b"000000C1", # Success ack b"0005" # response msg_id + b"00" # existing - + b"01", # allowed + + b"00", # not allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From 0f69e3467f45825777ab0e7fe2bce4eb0bf32254 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:28:46 +0100 Subject: [PATCH 052/123] Extra bit --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index fbe9e0750..d1c12f9b8 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -74,7 +74,7 @@ b"000000C1", # Success ack b"0005" # response msg_id + b"00" # existing - + b"00", # not allowed + + b"000", # not allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From 687de46c5916000481af5ca6edb72f8cc1cb446c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 08:32:54 +0100 Subject: [PATCH 053/123] CirclePlusConnectReqyest: shorter args --- plugwise_usb/messages/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 16b319635..8dc2b14ab 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 e5335fbad6b4f98f6c4666a9e980ccc9c27186f5 Mon Sep 17 00:00:00 2001 From: autoruff Date: Tue, 10 Feb 2026 07:34:30 +0000 Subject: [PATCH 054/123] fixup: pair-plus Python code fixed using Ruff --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index d1c12f9b8..c4d74f255 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -432,7 +432,7 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - await asyncio.sleep(2) + await asyncio.sleep(2) await stick.plus_pair_request("0098765432101234") await asyncio.sleep(2) From 2e473e644f0ba313dd4558361aaa6b6e41f7e79b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:04:26 +0100 Subject: [PATCH 055/123] Add stick-mac to 0005-response, remove extra bit --- tests/test_pairing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index c4d74f255..80c5a9513 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -73,8 +73,9 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id + + b"0123456789012345" # stick-mac + b"00" # existing - + b"000", # not allowed + + b"00", # not allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From 988c764bbb89724b2a6f566527677d5ade913a77 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 13:10:31 +0100 Subject: [PATCH 056/123] Ruffed --- plugwise_usb/network/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 50834a751..985cd7581 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -20,10 +20,7 @@ ) from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout from ..helpers.util import validate_mac -from ..messages.requests import ( - CircleMeasureIntervalRequest, - NodePingRequest, -) +from ..messages.requests import CircleMeasureIntervalRequest, NodePingRequest from ..messages.responses import ( NODE_AWAKE_RESPONSE_ID, NODE_JOIN_ID, From 9c56fbbad44f19210816ccba3f9e63134e7765fc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:31:16 +0100 Subject: [PATCH 057/123] Shorten args, must be length=16 --- plugwise_usb/messages/requests.py | 2 +- tests/test_pairing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 8dc2b14ab..6991dc196 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 dac83154c635b5e4eb604d7d56c73a9fba182e82 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 10 Feb 2026 17:47:09 +0100 Subject: [PATCH 058/123] Add missing CRC, can be corrected later --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 4f219e749..d41969a3d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x03000400000000000000000098765432101234\r\n": ( + b"\x05\x05\x03\x03000400000000000000000098765432101234ABCD\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From 84906aa3f066f53d77a7811046fafc980f74f2ab Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 12 Feb 2026 19:55:47 +0100 Subject: [PATCH 059/123] Try --- plugwise_usb/messages/requests.py | 2 +- tests/test_pairing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 6991dc196..16b319635 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 93637e3033da42ab70479846e6cd6d9cb1d736ae Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 07:59:09 +0100 Subject: [PATCH 060/123] Try 2 --- tests/test_pairing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 61b7dc249..370ad95af 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -433,9 +433,9 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: # with pytest.raises(pw_exceptions.StickError): await stick.initialize() - await asyncio.sleep(2) + await asyncio.sleep(0.2) await stick.plus_pair_request("0098765432101234") - await asyncio.sleep(2) + await asyncio.sleep(0.2) await stick.disconnect() From 5049ebaadaf0786642042cb8f3ea9fc9007219ad Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:03:48 +0100 Subject: [PATCH 061/123] Fixes --- tests/test_pairing.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 370ad95af..26a2fbc37 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -92,6 +92,29 @@ ), } +PARTLY_RESPONSE_MESSAGES = { + b"\x05\x05\x03\x0300161111111111111111": ( + "Clock set 1111111111111111", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"1111111111111111", # msg_id, ClockAccepted, mac + ), + b"\x05\x05\x03\x0300162222222222222222": ( + "Clock set 2222222222222222", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"2222222222222222", # msg_id, ClockAccepted, mac + ), + b"\x05\x05\x03\x0300163333333333333333": ( + "Clock set 3333333333333333", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"3333333333333333", # msg_id, ClockAccepted, mac + ), + b"\x05\x05\x03\x0300164444444444444444": ( + "Clock set 4444444444444444", + b"000000C1", # Success ack + b"0000" + b"00D7" + b"4444444444444444", # msg_id, ClockAccepted, mac + ), +} + SECOND_RESPONSE_MESSAGES = { b"\x05\x05\x03\x03000D55555555555555555E46\r\n": ( "ping reply for 5555555555555555", @@ -420,7 +443,7 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" - mock_serial = MockSerial(None) + # mock_serial = MockSerial(None) monkeypatch.setattr( pw_connection_manager, "create_serial_connection", From 4d8c17a11e8a147cb514db943992d1d825d1cde8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:05:23 +0100 Subject: [PATCH 062/123] Correct CRC --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 26a2fbc37..5e5342ee4 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -69,7 +69,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x030004000000000000000000000098765432101234ABCD\r\n": ( + b"\x05\x05\x03\x0300040000000000000000000000987654321012344D73\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id From 582f949956ed94402f9c333951d6c0ff9aab2121 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 08:08:49 +0100 Subject: [PATCH 063/123] Try allowed --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 5e5342ee4..8f1a88000 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -75,7 +75,7 @@ b"0005" # response msg_id + b"0123456789012345" # stick-mac + b"00" # existing - + b"00", # not allowed + + b"01", # allowed ), b"\x05\x05\x03\x0300230123456789012345A0EC\r\n": ( "Node Info of stick 0123456789012345", From bccfb8b09a856edde4fd8ab402fb39e0e77e095c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:35:20 +0100 Subject: [PATCH 064/123] Change to Circle+ mac in 0005-response --- tests/test_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 8f1a88000..582dee78d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -73,7 +73,7 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id - + b"0123456789012345" # stick-mac + + b"0098765432101234" # circle+ mac + b"00" # existing + b"01", # allowed ), From d31b921babfd78d2e619e85fa5ffec69160d8808 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 18:56:13 +0100 Subject: [PATCH 065/123] Ruff-cleanup --- tests/test_pairing.py | 110 +----------------------------------------- 1 file changed, 2 insertions(+), 108 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 582dee78d..bf1037497 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -2,18 +2,16 @@ import asyncio from collections.abc import Callable, Coroutine -from datetime import UTC, datetime as dt, timedelta as td import importlib import logging import random from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock import pytest import aiofiles # type: ignore[import-untyped] import crcmod -from freezegun import freeze_time crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000) @@ -350,7 +348,7 @@ async def send( aiofiles.threadpool.wrap.register(MagicMock)( - lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) # pylint: disable=unnecessary-lambda + lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) # noqa: PLW0108 pylint: disable=unnecessary-lambda ) @@ -380,66 +378,6 @@ async def dummy_fn(self, request: pw_requests.PlugwiseRequest, test: bool) -> No """Callable dummy routine.""" return - # @pytest.mark.asyncio - # async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None: - # """Test connecting to stick.""" - # monkeypatch.setattr( - # pw_connection_manager, - # "create_serial_connection", - # MockSerial(None).mock_connection, - # ) - # stick = pw_stick.Stick(port="test_port", cache_enabled=False) - - # unsub_connect = stick.subscribe_to_stick_events( - # stick_event_callback=self.connected, - # events=(pw_api.StickEvent.CONNECTED,), - # ) - # self.test_connected = asyncio.Future() - # await stick.connect("test_port") - # assert await self.test_connected - # await stick.initialize() - # assert stick.mac_stick == "0123456789012345" - # assert stick.name == "Stick 12345" - # assert stick.mac_coordinator == "0098765432101234" - # assert stick.firmware == dt(2011, 6, 27, 8, 47, 37, tzinfo=UTC) - # assert stick.hardware == "070085" - # assert not stick.network_discovered - # assert stick.network_state - # assert stick.network_id == 17185 - # unsub_connect() - # await stick.disconnect() - # assert not stick.network_state - # with pytest.raises(pw_exceptions.StickError): - # stick.mac_stick - - # @pytest.mark.asyncio - # async def test_stick_network_down(self, monkeypatch: pytest.MonkeyPatch) -> None: - # """Testing Stick init without paired Circle.""" - # mock_serial = MockSerial( - # { - # b"\x05\x05\x03\x03000AB43C\r\n": ( - # "STICK INIT", - # b"000000C1", # Success ack - # b"0011" # response msg_id - # + b"0123456789012345" # stick mac - # + b"00" # unknown1 - # + b"00", # network_is_offline - # ), - # } - # ) - # monkeypatch.setattr( - # pw_connection_manager, - # "create_serial_connection", - # mock_serial.mock_connection, - # ) - # monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2) - # monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0) - # stick = pw_stick.Stick(port="test_port", cache_enabled=False) - # await stick.connect() - # with pytest.raises(pw_exceptions.StickError): - # await stick.initialize() - # await stick.disconnect() - @pytest.mark.asyncio async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test pairing a plus-device.""" @@ -453,7 +391,6 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) stick = pw_stick.Stick(port="test_port", cache_enabled=False) await stick.connect("test_port") - # with pytest.raises(pw_exceptions.StickError): await stick.initialize() await asyncio.sleep(0.2) @@ -461,46 +398,3 @@ async def test_pair_plus(self, monkeypatch: pytest.MonkeyPatch) -> None: await asyncio.sleep(0.2) await stick.disconnect() - - -# async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] -# """Handle join event callback.""" -# if event == pw_api.NodeEvent.JOIN: -# self.test_node_join.set_result(mac) -# else: -# self.test_node_join.set_exception( -# BaseException( -# f"Invalid {event} event, expected " + f"{pw_api.NodeEvent.JOIN}" -# ) -# ) - -# @pytest.mark.asyncio -# async def test_stick_node_join_subscription( -# self, monkeypatch: pytest.MonkeyPatch -# ) -> None: -# """Testing "new_node" subscription.""" -# mock_serial = MockSerial(None) -# monkeypatch.setattr( -# pw_connection_manager, -# "create_serial_connection", -# mock_serial.mock_connection, -# ) -# monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1) -# monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 0.5) -# stick = pw_stick.Stick("test_port", cache_enabled=False) -# await stick.connect() -# await stick.initialize() -# await stick.discover_nodes(load=False) - -# self.test_node_join = asyncio.Future() -# unusb_join = stick.subscribe_to_node_events( -# node_event_callback=self.node_join, -# events=(pw_api.NodeEvent.JOIN,), -# ) - -## Inject NodeJoinAvailableResponse -# mock_serial.inject_message(b"00069999999999999999", b"1253") # @bouwew: seq_id is not FFFC! -# mac_join_node = await self.test_node_join -# assert mac_join_node == "9999999999999999" -# unusb_join() -# await stick.disconnect() From 6c8b4e76bc144a9866edf08ee089f1289a47074b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:15:14 +0100 Subject: [PATCH 066/123] Bump to v0.48.0a1 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0ccb1d1ae..c958662a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.47.7" +version = "0.48.0a1" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 0ed5f70a9d1f7d38fc48cf0be7220bb101c54a20 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Feb 2026 19:17:07 +0100 Subject: [PATCH 067/123] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1839dd0dc..55c2aff6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,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 18e5fed35f4e5634a3b30da3cbe478b29de9980d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:41:52 +0100 Subject: [PATCH 068/123] Implement StickInitShortResponse-handling in class StickController --- plugwise_usb/connection/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 507589cb0..77e8e1c06 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -23,6 +23,7 @@ NodePingResponse, PlugwiseResponse, StickInitResponse, + StickInitShortResponse, ) from .manager import StickConnectionManager from .queue import StickQueue @@ -172,7 +173,9 @@ async def initialize_stick(self) -> None: try: request = StickInitRequest(self.send) - init_response: StickInitResponse | None = await request.send() + init_response: ( + StickInitResponse | StickInitShortResponse | None + ) = await request.send() except StickError as err: raise StickError( "No response from USB-Stick to initialization request." @@ -188,10 +191,11 @@ async def initialize_stick(self) -> None: self._mac_stick = init_response.mac_decoded self.stick_name = f"Stick {self._mac_stick[-5:]}" self._network_online = init_response.network_online + if self._network_online: + # Replace first 2 characters by 00 for mac of circle+ node + self._mac_nc = init_response.mac_network_controller + self._network_id = init_response.network_id - # Replace first 2 characters by 00 for mac of circle+ node - self._mac_nc = init_response.mac_network_controller - self._network_id = init_response.network_id self._is_initialized = True # Add Stick NodeInfoRequest @@ -201,9 +205,6 @@ async def initialize_stick(self) -> None: hardware, _ = version_to_model(node_info.hardware) self._hw_stick = hardware - if not self._network_online: - raise StickError("Zigbee network connection to Circle+ is down.") - async def pair_plus_device(self, mac: str) -> bool: """Pair Plus-device to Plugwise Stick. From b19008351c878316aa674404e01b1adea1b9a01b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 17:44:42 +0100 Subject: [PATCH 069/123] Back to full test-output --- scripts/tests_and_coverage.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 5c0ad8ac7..54fe2b020 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,8 +56,7 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - # PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || - PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py + PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From f245e9d1261018432f50d39f2e574c45c8ac6a45 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:22:04 +0100 Subject: [PATCH 070/123] Improve --- plugwise_usb/messages/requests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 16b319635..fdfb81feb 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 b07b1c3c7a5343035beb9482535d80a76cc14491 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:28:28 +0100 Subject: [PATCH 071/123] Correct CHANGELOG after rebase --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c2aff6e..e5a679c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,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 a0e2a95f1af7498aef9aeffe73326094dec1ca04 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:29:04 +0100 Subject: [PATCH 072/123] Bump to v0.48.0a2 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c958662a6..eee12de3f 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 c9593d1d3261dae4da568b205b55899b99464c09 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 16 Feb 2026 18:31:46 +0100 Subject: [PATCH 073/123] Run all test-files in case of failure --- scripts/tests_and_coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 54fe2b020..e3b7ee971 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -56,7 +56,7 @@ set +u if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then # Python tests (rerun with debug if failures) - PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_pairing.py + PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/ fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then From 288e6e8da83ce2df93db344777247d0b47ccb0e4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 10:45:38 +0100 Subject: [PATCH 074/123] Revert back to python 3.13 --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 1a81ae365..92185c48e 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@v6 From fe97fd7fd78650dffac37cca07c8956551d6c212 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 11:03:36 +0100 Subject: [PATCH 075/123] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eee12de3f..f6fe4dd97 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 1cb1aca89c6b203ee91db41458ea9325dff2ee88 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:33:49 +0100 Subject: [PATCH 076/123] Try-except stick-initialize --- plugwise_usb/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index fa4de8144..3259d960b 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -273,7 +273,11 @@ async def plus_pair_request(self, mac: str) -> bool: @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" - await self._controller.initialize_stick() + try: + await self._controller.initialize_stick() + except StickError as exc: + raise StickError(f"Cannot initialize Stick-connection: {exc}") from exc + if self._network is None: self._network = StickNetwork(self._controller) self._network.cache_folder = self._cache_folder From 9695e6475ee5afc15cf7c1831fd4eac7c6215e5d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:46:16 +0100 Subject: [PATCH 077/123] Ruff fix --- plugwise_usb/messages/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index fdfb81feb..87ad374fb 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 49642646cc64b8080f4409b91510badbf4ad62f7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:48:49 +0100 Subject: [PATCH 078/123] Add log-warning --- plugwise_usb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 3259d960b..e8d038098 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -276,6 +276,7 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: try: await self._controller.initialize_stick() except StickError as exc: + _LOGGER.warning("Cannot initialize Stick-connection: %s", exc) raise StickError(f"Cannot initialize Stick-connection: {exc}") from exc if self._network is None: From 43912fee25a1a6a840c93b8ce69fec6174c03ec5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 17 Feb 2026 19:51:40 +0100 Subject: [PATCH 079/123] Bump to a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6fe4dd97..d5545ea0d 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 0430479f51097fadea97d041a7c32de6b4c430ed Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:52:47 +0100 Subject: [PATCH 080/123] Remove is_connected requirement for mac_stick --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 77e8e1c06..e0d821aae 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -74,7 +74,7 @@ def hardware_stick(self) -> str | None: @property def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" - if not self._manager.is_connected or self._mac_stick is None: + if self._mac_stick is None: raise StickError( "No mac address available. Connect and initialize USB-Stick first." ) From 9c9117d03ee57ae2f0df9f6d3f0c2b12d53fa1fd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 16:53:14 +0100 Subject: [PATCH 081/123] Bump to a5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d5545ea0d..de8d72d77 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 0fad6008d57848a650ac26ef2bf5d21c4b7f0ca4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 18 Feb 2026 17:03:13 +0100 Subject: [PATCH 082/123] Disable now invalid test --- tests/test_usb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 59e62cd44..b25d911a0 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 f3b5ecf8099c039f3850bdb94358edb817c76166 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:07 +0100 Subject: [PATCH 083/123] More debug-logging --- plugwise_usb/connection/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index e0d821aae..42ff88bbf 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -75,6 +75,7 @@ def hardware_stick(self) -> str | None: def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" if self._mac_stick is None: + _LOGGER.debug("mac_stick: %s", self._mac_stick) raise StickError( "No mac address available. Connect and initialize USB-Stick first." ) @@ -86,6 +87,8 @@ def mac_coordinator(self) -> str: Raises StickError when not connected. """ + _LOGGER.debug("mac_coordinator: %s", self._mac_nc) + _LOGGER.debug("is_connected: %s", self._manager.is_connected) if not self._manager.is_connected or self._mac_nc is None: raise StickError( "No mac address available. Connect and initialize USB-Stick first." From b7421a99544cdee2df43a015f89ba5c5bf60e073 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 13:29:26 +0100 Subject: [PATCH 084/123] Bump to a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de8d72d77..1034a0c45 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 7afe29b870b6090d309cd5f7ae9c1ac4025bc0fb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:05:34 +0100 Subject: [PATCH 085/123] Move debug message --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 42ff88bbf..cbca49add 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -74,8 +74,8 @@ def hardware_stick(self) -> str | None: @property def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" + _LOGGER.debug("mac_stick: %s", self._mac_stick) if self._mac_stick is None: - _LOGGER.debug("mac_stick: %s", self._mac_stick) raise StickError( "No mac address available. Connect and initialize USB-Stick first." ) From 867e24a2f4c098337be7fa8ada434f784e831efd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:06:15 +0100 Subject: [PATCH 086/123] Bump to a7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1034a0c45..7b6b6265c 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 fd8c2f0d276201619d7e823160cf96540d04349c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 19 Feb 2026 14:08:54 +0100 Subject: [PATCH 087/123] Revert adding try-except --- plugwise_usb/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index e8d038098..fa4de8144 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -273,12 +273,7 @@ async def plus_pair_request(self, mac: str) -> bool: @raise_not_connected async def initialize(self, create_root_cache_folder: bool = False) -> None: """Initialize connection to USB-Stick.""" - try: - await self._controller.initialize_stick() - except StickError as exc: - _LOGGER.warning("Cannot initialize Stick-connection: %s", exc) - raise StickError(f"Cannot initialize Stick-connection: {exc}") from exc - + await self._controller.initialize_stick() if self._network is None: self._network = StickNetwork(self._controller) self._network.cache_folder = self._cache_folder From 851dcfe07243dad9f707abfed1b7eb47bcdb7abe Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:02:30 +0100 Subject: [PATCH 088/123] Replace debuggers by distinct message --- plugwise_usb/connection/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index cbca49add..0f848d430 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -74,10 +74,9 @@ def hardware_stick(self) -> str | None: @property def mac_stick(self) -> str: """MAC address of USB-Stick. Raises StickError when not connected.""" - _LOGGER.debug("mac_stick: %s", self._mac_stick) if self._mac_stick is None: raise StickError( - "No mac address available. Connect and initialize USB-Stick first." + "No mac_stick address available. Connect and initialize USB-Stick first." ) return self._mac_stick @@ -87,11 +86,9 @@ def mac_coordinator(self) -> str: Raises StickError when not connected. """ - _LOGGER.debug("mac_coordinator: %s", self._mac_nc) - _LOGGER.debug("is_connected: %s", self._manager.is_connected) if not self._manager.is_connected or self._mac_nc is None: raise StickError( - "No mac address available. Connect and initialize USB-Stick first." + "No mac_nc address available. Connect and initialize USB-Stick first." ) return self._mac_nc From 12be5ad877786d12717faaa0c9aacdb0569e2baa Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:04:26 +0100 Subject: [PATCH 089/123] Bump to a8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7b6b6265c..681661fc4 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 6a9b5af825d959b413fef0aac8f76ffce4189130 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 08:56:53 +0100 Subject: [PATCH 090/123] Remove unneeded StickError raises --- plugwise_usb/connection/__init__.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 0f848d430..af3f1fcdc 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -72,33 +72,18 @@ def hardware_stick(self) -> str | None: return self._hw_stick @property - def mac_stick(self) -> str: - """MAC address of USB-Stick. Raises StickError when not connected.""" - if self._mac_stick is None: - raise StickError( - "No mac_stick address available. Connect and initialize USB-Stick first." - ) + def mac_stick(self) -> str ? None: + """MAC address of USB-Stick.""" return self._mac_stick @property - def mac_coordinator(self) -> str: - """Return MAC address of the Zigbee network coordinator (Circle+). - - Raises StickError when not connected. - """ - if not self._manager.is_connected or self._mac_nc is None: - raise StickError( - "No mac_nc address available. Connect and initialize USB-Stick first." - ) + def mac_coordinator(self) -> str | None: + """Return MAC address of the Zigbee network coordinator (Circle+).""" return self._mac_nc @property - def network_id(self) -> int: - """Returns the Zigbee network ID. Raises StickError when not connected.""" - if not self._manager.is_connected or self._network_id is None: - raise StickError( - "No network ID available. Connect and initialize USB-Stick first." - ) + def network_id(self) -> int | None: + """Returns the Zigbee network ID.""" return self._network_id @property From 2cf0fd6584ba81351b454bb1f87ed2f9c495b079 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:01:58 +0100 Subject: [PATCH 091/123] Update relevant test-asserts --- tests/test_usb.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index b25d911a0..2a8823f03 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 eb1f122e37f759886ac2bc768a8fbe02073af487 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:05:41 +0100 Subject: [PATCH 092/123] Ruff fixes --- plugwise_usb/connection/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index af3f1fcdc..7ff75901d 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -72,7 +72,7 @@ def hardware_stick(self) -> str | None: return self._hw_stick @property - def mac_stick(self) -> str ? None: + def mac_stick(self) -> str | None: """MAC address of USB-Stick.""" return self._mac_stick @@ -186,7 +186,7 @@ async def initialize_stick(self) -> None: # Add Stick NodeInfoRequest node_info, _ = await self.get_node_details(self._mac_stick, ping_first=False) if node_info is not None: - self._fw_stick = node_info.firmware + self._fw_stick = node_info.firmware # type: ignore hardware, _ = version_to_model(node_info.hardware) self._hw_stick = hardware From 5cbcb5b9b7288d6dd750b15e9234d4f0ea762be4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:24:55 +0100 Subject: [PATCH 093/123] Correct -update test_stick_network_down() --- tests/test_usb.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 2a8823f03..3bc9c1300 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 04c8d3f99784b360bb950d22cf6f9df16687ae69 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:29:50 +0100 Subject: [PATCH 094/123] Fix pylint warnings --- tests/test_usb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 3bc9c1300..835fcb2c4 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 223e2fa5c6054e3f82f5835a42164dc3fe630778 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 09:46:58 +0100 Subject: [PATCH 095/123] More adapting to StickInitShortResponse --- plugwise_usb/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index fa4de8144..3ecfee62a 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -237,8 +237,8 @@ async def setup(self, discover: bool = True, load: bool = True) -> None: if not self.is_connected: await self.connect() if not self.is_initialized: - await self.initialize() - if discover: + initialized = await self.initialize() + if initialized and discover: await self.start_network() await self.discover_coordinator() await self.discover_nodes() @@ -271,9 +271,13 @@ async def plus_pair_request(self, mac: str) -> bool: return await self._controller.pair_plus_device(mac) @raise_not_connected - async def initialize(self, create_root_cache_folder: bool = False) -> None: + async def initialize(self, create_root_cache_folder: bool = False) -> bool: """Initialize connection to USB-Stick.""" await self._controller.initialize_stick() + # Check if network is offline = StickInitShortResponse + if self._controller.mac_coordinator is None: + return False + if self._network is None: self._network = StickNetwork(self._controller) self._network.cache_folder = self._cache_folder @@ -282,6 +286,8 @@ async def initialize(self, create_root_cache_folder: bool = False) -> None: if self._cache_enabled: await self._network.initialize_cache() + return True + @raise_not_connected @raise_not_initialized async def start_network(self) -> None: From 39d3ca198d621f517c6cd32f4f849bd80075169a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:12:14 +0100 Subject: [PATCH 096/123] Bump to a9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 681661fc4..643809c79 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 d12880afe3b72b21233f4655b333f9c4c913dc27 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:40:51 +0100 Subject: [PATCH 097/123] Update Stick properties mac_stick, mac_coordinator and name --- plugwise_usb/__init__.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 3ecfee62a..476a354d8 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -130,18 +130,27 @@ def hardware(self) -> str: return self._controller.hardware_stick @property - def mac_stick(self) -> str: - """MAC address of USB-Stick. Raises StickError is connection is missing.""" + def mac_stick(self) -> str | None: + """MAC address of USB-Stick. + + Returns None when the connection to the Stick fails. + """ return self._controller.mac_stick @property - def mac_coordinator(self) -> str: - """MAC address of the network coordinator (Circle+). Raises StickError is connection is missing.""" + def mac_coordinator(self) -> str | None: + """MAC address of the network coordinator (Circle+). + + Returns none when there is no connection, not paired, not present in the network. + """ return self._controller.mac_coordinator @property - def name(self) -> str: - """Return name of Stick.""" + def name(self) -> str | None: + """Return name of Stick. + + Returns None when the connection to the Stick fails. + """ return self._controller.stick_name @property From 4eb1b479c3f550209d52c68ccd156808161f43cb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 10:51:53 +0100 Subject: [PATCH 098/123] Update docstring --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 7ff75901d..3069a9816 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -246,7 +246,7 @@ async def pair_plus_device(self, mac: str) -> bool: async def get_node_details( self, mac: str, ping_first: bool ) -> tuple[NodeInfoResponse | None, NodePingResponse | None]: - """Return node discovery type.""" + """Collect NodeInfo data from the Stick.""" ping_response: NodePingResponse | None = None if ping_first: # Define ping request with one retry From 34256a728e9e949ebe652f290275e6f515eb7552 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 12:20:46 +0100 Subject: [PATCH 099/123] Update network_online docstring --- plugwise_usb/connection/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 3069a9816..4fbb940cb 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -88,7 +88,11 @@ def network_id(self) -> int | None: @property def network_online(self) -> bool: - """Return the network state.""" + """Return the network state. + + The ZigBee network is online when the Stick is connected and a + StickInitResponse indicates that the ZigBee network is online. + """ if not self._manager.is_connected: raise StickError( "Network status not available. Connect and initialize USB-Stick first." From 2a550f1674579189c352879c87bd506f4fa60ff6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 20 Feb 2026 14:21:56 +0100 Subject: [PATCH 100/123] Bump to a10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 643809c79..8f83c0364 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 f07895cee42aacfc332caa14c7b3eb370a0e8937 Mon Sep 17 00:00:00 2001 From: autoruff Date: Fri, 20 Feb 2026 13:22:59 +0000 Subject: [PATCH 101/123] fixup: pair-plus Python code fixed using Ruff --- plugwise_usb/__init__.py | 6 +++--- plugwise_usb/connection/__init__.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 476a354d8..7bcc546fb 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -132,7 +132,7 @@ def hardware(self) -> str: @property def mac_stick(self) -> str | None: """MAC address of USB-Stick. - + Returns None when the connection to the Stick fails. """ return self._controller.mac_stick @@ -140,7 +140,7 @@ def mac_stick(self) -> str | None: @property def mac_coordinator(self) -> str | None: """MAC address of the network coordinator (Circle+). - + Returns none when there is no connection, not paired, not present in the network. """ return self._controller.mac_coordinator @@ -148,7 +148,7 @@ def mac_coordinator(self) -> str | None: @property def name(self) -> str | None: """Return name of Stick. - + Returns None when the connection to the Stick fails. """ return self._controller.stick_name diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 4fbb940cb..a935d2765 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -90,7 +90,7 @@ def network_id(self) -> int | None: def network_online(self) -> bool: """Return the network state. - The ZigBee network is online when the Stick is connected and a + The ZigBee network is online when the Stick is connected and a StickInitResponse indicates that the ZigBee network is online. """ if not self._manager.is_connected: @@ -279,7 +279,7 @@ async def send( return await self._queue.submit(request) try: return await self._queue.submit(request) - except (NodeError, StickError): + except NodeError, StickError: return None def _reset_states(self) -> None: From 416ebc973d40ebf67d8d764de9ea2bad47cb5d29 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 09:46:34 +0100 Subject: [PATCH 102/123] Responses: line up Int() use --- plugwise_usb/messages/responses.py | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 9f620216e..30acd7c6b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -332,7 +332,7 @@ class NodeSpecificResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeSpecificResponse message object.""" super().__init__(b"0003") - self.status = Int(0, 4) + self.status = Int(0, length=4) self._params += [self.status] @@ -346,8 +346,8 @@ class CirclePlusConnectResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize CirclePlusConnectResponse message object.""" super().__init__(b"0005") - self.existing = Int(0, 2) - self.allowed = Int(0, 2) + self.existing = Int(0, length=2) + self.allowed = Int(0, length=2) self._params += [self.existing, self.allowed] @@ -500,13 +500,13 @@ class CirclePowerUsageResponse(PlugwiseResponse): def __init__(self, protocol_version: str = "2.3") -> None: """Initialize CirclePowerUsageResponse message object.""" super().__init__(b"0013") - self._pulse_1s = Int(0, 4) - self._pulse_8s = Int(0, 4) - self._nanosecond_offset = Int(0, 4) + self._pulse_1s = Int(0, length=4) + self._pulse_8s = Int(0, length=4) + self._nanosecond_offset = Int(0, length=4) self._params += [self._pulse_1s, self._pulse_8s] if protocol_version == "2.3": - self._pulse_counter_consumed = Int(0, 8) - self._pulse_counter_produced = Int(0, 8) + self._pulse_counter_consumed = Int(0, length=8) + self._pulse_counter_produced = Int(0, length=8) self._params += [ self._pulse_counter_consumed, self._pulse_counter_produced, @@ -608,7 +608,7 @@ def __init__(self) -> None: """Initialize NodeRemoveResponse message object.""" super().__init__(b"001D") self.node_mac_id = String(None, length=16) - self.status = Int(0, 2) + self.status = Int(0, length=2) self._params += [self.node_mac_id, self.status] @@ -757,8 +757,8 @@ def __init__(self) -> None: super().__init__(b"003F") self.time = Time() self.day_of_week = Int(0, 2, False) - self.unknown = Int(0, 2) - self.unknown2 = Int(0, 4) + self.unknown = Int(0, length=2) + self.unknown2 = Int(0, length=4) self._params += [ self.time, self.day_of_week, @@ -779,13 +779,13 @@ def __init__(self) -> None: """Initialize CircleEnergyLogsResponse message object.""" super().__init__(b"0049") self.logdate1 = DateTime() - self.pulses1 = Int(0, 8) + self.pulses1 = Int(0, length=8) self.logdate2 = DateTime() - self.pulses2 = Int(0, 8) + self.pulses2 = Int(0, length=8) self.logdate3 = DateTime() - self.pulses3 = Int(0, 8) + self.pulses3 = Int(0, length=8) self.logdate4 = DateTime() - self.pulses4 = Int(0, 8) + self.pulses4 = Int(0, length=8) self._logaddr = LogAddr(0, length=8) self._params += [ self.logdate1, @@ -969,7 +969,7 @@ class SenseReportResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize SenseReportResponse message object.""" super().__init__(SENSE_REPORT_ID) - self.humidity = Int(0, length=4, negative=False) + self.humidity = Int(0, 4, False) self.temperature = Int(0, length=4) self._params += [self.humidity, self.temperature] From 30d3342d50de4ba8063ae002aab33b8d149422ff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 09:59:36 +0100 Subject: [PATCH 103/123] Add missing decode_mac=False --- plugwise_usb/messages/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 30acd7c6b..7d1fa6481 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -288,7 +288,7 @@ class StickNetworkInfoResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeNetworkInfoResponse message object.""" - super().__init__(b"0002") + super().__init__(b"0002", decode_mac=False) self._channel = Int(0, length=2) self._source_mac_id = String(None, length=16) self.extended_pan_id = String(None, length=16) @@ -331,7 +331,7 @@ class NodeSpecificResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize NodeSpecificResponse message object.""" - super().__init__(b"0003") + super().__init__(b"0003", decode_mac=False) self.status = Int(0, length=4) self._params += [self.status] From 9125ebb301b3c99cd34c05a446d4a574fc562fe3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 10:26:34 +0100 Subject: [PATCH 104/123] Correct 0002-format in response --- tests/test_pairing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index bf1037497..27da7b99f 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -47,7 +47,6 @@ "Stick network info request", b"000000C1", # Success ack b"0002" # response msg_id - + b"0123456789012345" # stick-mac + b"0F" # channel + b"FFFFFFFFFFFFFFFF" + b"FF98765432101234" # 06 + plus-device mac From 7c6c2cc3328279b609707bae32d8f44901c91901 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 10:47:55 +0100 Subject: [PATCH 105/123] Bump to a11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8f83c0364..ae9e9622c 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 045e1786c7cfd52ef00f0e223bf937b2937e04da Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 11:18:38 +0100 Subject: [PATCH 106/123] Exit when network is not online --- plugwise_usb/connection/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index a935d2765..ab9467004 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,7 +220,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None - _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -229,7 +228,8 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - _LOGGER.debug("HOI Init done") + if not self._network_online: + raise NodeError("Pairing failed, network is not online") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,8 +243,6 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") - _LOGGER.debug("HOI PlusConnectRequest done") - return True async def get_node_details( From c92280d12a99f44d0eebf4c2cc65fe93c766d718 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 11:24:56 +0100 Subject: [PATCH 107/123] Bump to a12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ae9e9622c..805914e34 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 a632bd04e5a4413a76736c1b9711809ba6eb86d3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:07:07 +0100 Subject: [PATCH 108/123] Revert "Exit when network is not online" This reverts commit 9d5ba855f5ba5c2dbed599b452f00382ee62157e. --- plugwise_usb/connection/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index ab9467004..a935d2765 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,6 +220,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None + _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -228,8 +229,7 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - if not self._network_online: - raise NodeError("Pairing failed, network is not online") + _LOGGER.debug("HOI Init done") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,6 +243,8 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") + _LOGGER.debug("HOI PlusConnectRequest done") + return True async def get_node_details( From 0a188ea7482ba7b877080aa1199a9b2eaa29fa4a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:08:38 +0100 Subject: [PATCH 109/123] Remove logger-HOI-lines --- plugwise_usb/connection/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index a935d2765..2f89f1059 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -220,7 +220,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( "Pairing failed, StickNetworkInfoResponse is None" ) from None - _LOGGER.debug("HOI NetworkInfoRequest done") # Init Stick try: @@ -229,7 +228,6 @@ async def pair_plus_device(self, mac: str) -> bool: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" ) from exc - _LOGGER.debug("HOI Init done") try: request = CirclePlusConnectRequest(self.send, bytes(mac, UTF8)) @@ -243,8 +241,6 @@ async def pair_plus_device(self, mac: str) -> bool: if response.allowed.value != 1: raise NodeError("Pairing failed, not allowed") - _LOGGER.debug("HOI PlusConnectRequest done") - return True async def get_node_details( From 5a355f219873d25efb046d95a259a73523b0d144 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 14:18:43 +0100 Subject: [PATCH 110/123] Bump to a13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 805914e34..79ee60484 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 93bec4a42659bb0328a8d0f014a2cac38547c346 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 15:28:44 +0100 Subject: [PATCH 111/123] Don't collect NodeInfo during pairing --- plugwise_usb/connection/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 2f89f1059..640eec4ce 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -151,7 +151,7 @@ async def _handle_stick_event(self, event: StickEvent) -> None: elif event == StickEvent.DISCONNECTED and self._queue.is_running: await self._queue.stop() - async def initialize_stick(self) -> None: + async def initialize_stick(self, node_info=True) -> None: """Initialize connection to the USB-stick.""" if not self._manager.is_connected: raise StickError( @@ -187,7 +187,10 @@ async def initialize_stick(self) -> None: self._is_initialized = True - # Add Stick NodeInfoRequest + if not node_info: + return + + # Collect Stick NodeInfo node_info, _ = await self.get_node_details(self._mac_stick, ping_first=False) if node_info is not None: self._fw_stick = node_info.firmware # type: ignore @@ -223,7 +226,7 @@ async def pair_plus_device(self, mac: str) -> bool: # Init Stick try: - await self.initialize_stick() + await self.initialize_stick(node_info=False) except StickError as exc: raise NodeError( f"Pairing failed, failed to initialize Stick: {exc}" From 6bae1fd6383d0275a09a76296b14173a2d518072 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 16:23:21 +0100 Subject: [PATCH 112/123] Update 0004-request --- plugwise_usb/messages/requests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 87ad374fb..49f8240f0 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 a76519a708c3a0b69f4040b9ff94a69eb266c078 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 16:36:04 +0100 Subject: [PATCH 113/123] Update test-related --- tests/test_pairing.py | 2 +- tests/test_usb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 27da7b99f..a6c33adec 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -66,7 +66,7 @@ + b"04FF" + b"FF", ), - b"\x05\x05\x03\x0300040000000000000000000000987654321012344D73\r\n": ( + b"\x05\x05\x03\x030004000100000000000000000098765432101234C0AF\r\n": ( "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id diff --git a/tests/test_usb.py b/tests/test_usb.py index 835fcb2c4..3df04c220 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 402fb2e7f584f28ce226db89664fd61fe8945fe4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 18:59:09 +0100 Subject: [PATCH 114/123] Ruffed --- plugwise_usb/messages/requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 49f8240f0..766e3d83a 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 33167b5046ac26575ba49f8d63074250aa156a59 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Feb 2026 19:06:30 +0100 Subject: [PATCH 115/123] Update docstring --- plugwise_usb/messages/requests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 766e3d83a..cbc3ff4aa 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 e28f0af556fa44878dd29f1b52b48fe48d9ec930 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 08:59:41 +0100 Subject: [PATCH 116/123] Bump to a14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 79ee60484..c0c551f94 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 d727d0f35daabec6114a7fe7b7802c4ae7946e70 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 09:29:53 +0100 Subject: [PATCH 117/123] 0005-response: add missing decode_mac=False --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 7d1fa6481..998871d8b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -345,7 +345,7 @@ class CirclePlusConnectResponse(PlugwiseResponse): def __init__(self) -> None: """Initialize CirclePlusConnectResponse message object.""" - super().__init__(b"0005") + super().__init__(b"0005", decode_mac=False) self.existing = Int(0, length=2) self.allowed = Int(0, length=2) self._params += [self.existing, self.allowed] From 9268c3b7a35daa12325f8f4fb49c6e8050764492 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 24 Feb 2026 09:47:33 +0100 Subject: [PATCH 118/123] Improve docstring --- plugwise_usb/connection/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 640eec4ce..59ceab20e 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -202,12 +202,16 @@ async def pair_plus_device(self, mac: str) -> bool: According to https://roheve.wordpress.com/author/roheve/page/2/ The pairing process should look like: - 0001 - 0002 (- 0003): StickNetworkInfoRequest - StickNetworkInfoResponse - (PlugwiseQueryCirclePlusEndResponse - @SevenW), - 000A - 0011: StickInitRequest - StickInitResponse, + 0001 - 0002 - 0003: StickNetworkInfoRequest - StickNetworkInfoResponse - NodeSpecificResponse, + 000A - 0011: StickInitRequest - StickInitShortResponse/StickInitResponse, 0004 - 0005: CirclePlusConnectRequest - CirclePlusConnectResponse, the Plus-device will then send a NodeRejoinResponse (0061). - Todo(?): Does this need repeating until pairing is successful? + In the first occurrence of this process a 0004 0001 .... message is sent. + A StickInitShortResponse is received indicating the network is offline. + In the second occurrence of this process a 0004 0101 .... message is sent. + Again a StickInitShortResponse is received. + In the third occurrence only 000A is sent and a StickInitResponse indicating the network is online, is received. """ _LOGGER.debug("Pair Plus-device with mac: %s", mac) if not validate_mac(mac): From 3e9811d35ba603b57b8cae153e2c41c691824092 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 2 Mar 2026 18:28:32 +0100 Subject: [PATCH 119/123] Adapt 0005-test-response --- tests/test_pairing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index a6c33adec..f62398c7d 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -70,7 +70,6 @@ "Pair request of plus-device 0098765432101234", b"000000C1", # Success ack b"0005" # response msg_id - + b"0098765432101234" # circle+ mac + b"00" # existing + b"01", # allowed ), From f15f72c493fc36feb171cfc5c358ffd63e421fd5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 3 Mar 2026 08:23:51 +0100 Subject: [PATCH 120/123] Add init to StickNetworkInfoRequest --- plugwise_usb/messages/requests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index cbc3ff4aa..db844f65f 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 6455492ac473877918ca8d51a05c2bec75f17a12 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Tue, 3 Mar 2026 08:24:48 +0100 Subject: [PATCH 121/123] And adapt use --- plugwise_usb/connection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 59ceab20e..aaa8f5748 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -219,7 +219,7 @@ async def pair_plus_device(self, mac: str) -> bool: # Collect network info try: - request = StickNetworkInfoRequest(self.send, None) + request = StickNetworkInfoRequest(self.send) info_response = await request.send() except MessageError as exc: raise NodeError(f"Pairing failed: {exc}") from exc From cd70505314e4ffbcc6e052f4931aad9b677861c9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Mar 2026 20:02:48 +0100 Subject: [PATCH 122/123] Disable guarding that breaks 0004-0005 sequence detection --- plugwise_usb/connection/receiver.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/connection/receiver.py b/plugwise_usb/connection/receiver.py index 1dee3f8a4..6cf9103da 100644 --- a/plugwise_usb/connection/receiver.py +++ b/plugwise_usb/connection/receiver.py @@ -441,11 +441,11 @@ async def _notify_node_response_subscribers( notify_tasks: list[Coroutine[Any, Any, bool]] = [] for node_subscription in self._node_response_subscribers.values(): - if ( - node_subscription.mac is not None - and node_subscription.mac != node_response.mac - ): - continue + # if ( + # node_subscription.mac is not None + # and node_subscription.mac != node_response.mac + # ): + # continue if ( node_subscription.response_ids is not None and node_response.identifier not in node_subscription.response_ids From 975a6c61b1f7297193a2421fc8b40c35583059ec Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Mar 2026 20:11:51 +0100 Subject: [PATCH 123/123] Fix CHANGELOG after rebase --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a679c99..3a8dd5361 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.7 - 2026-05-18 PR [443](https://github.com/plugwise/python-plugwise-usb/pull/443): Migrate to serialx @@ -18,8 +22,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