Skip to content

Commit d899dd1

Browse files
authored
Merge pull request #337 from plugwise/improve-unregister-2
Implement node-reset before unregister
2 parents 26d5994 + f06b67b commit d899dd1

File tree

9 files changed

+69
-30
lines changed

9 files changed

+69
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Ongoing
44

5+
- PR [337](https://github.com/plugwise/python-plugwise-usb/pull/337): Improve node removal, remove and reset the node as executed by Source, and remove the cache-file.
56
- PR [342](https://github.com/plugwise/python-plugwise-usb/pull/342): Improve node_type chaching.
67

78
## 0.46.0 - 2025-09-12

plugwise_usb/messages/requests.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,10 @@ class NodeResetRequest(PlugwiseRequest):
481481
"""TODO:Some kind of reset request.
482482
483483
Supported protocols : 1.0, 2.0, 2.1
484-
Response message : <UNKNOWN>
484+
Response message : NodeResponse with NODE_RESET_ACK/NACK (@dirixmjm & @bouwew 20250910)
485485
"""
486486

487487
_identifier = b"0009"
488-
_reply_identifier = b"0003"
489488

490489
def __init__(
491490
self,
@@ -496,20 +495,18 @@ def __init__(
496495
) -> None:
497496
"""Initialize NodeResetRequest message object."""
498497
super().__init__(send_fn, mac)
499-
self._args += [
500-
Int(moduletype, length=2),
501-
Int(timeout, length=2),
502-
]
498+
module_id = getattr(moduletype, "value", moduletype)
499+
self._args += [Int(module_id, length=2), Int(timeout, length=2)]
503500

504-
async def send(self) -> NodeSpecificResponse | None:
501+
async def send(self) -> NodeResponse | None:
505502
"""Send request."""
506503
result = await self._send_request()
507-
if isinstance(result, NodeSpecificResponse):
504+
if isinstance(result, NodeResponse):
508505
return result
509506
if result is None:
510507
return None
511508
raise MessageError(
512-
f"Invalid response message. Received {result.__class__.__name__}, expected NodeSpecificResponse"
509+
f"Invalid response message. Received {result.__class__.__name__}, expected NodeResponse"
513510
)
514511

515512

@@ -863,11 +860,11 @@ def __init__(
863860
self,
864861
send_fn: Callable[[PlugwiseRequest, bool], Awaitable[PlugwiseResponse | None]],
865862
mac_circle_plus: bytes,
866-
mac_to_unjoined: str,
863+
mac_to_unjoin: str,
867864
) -> None:
868865
"""Initialize NodeRemoveRequest message object."""
869866
super().__init__(send_fn, mac_circle_plus)
870-
self._args.append(String(mac_to_unjoined, length=16))
867+
self._args.append(String(mac_to_unjoin, length=16))
871868

872869
async def send(self) -> NodeRemoveResponse | None:
873870
"""Send request."""

plugwise_usb/messages/responses.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@ class NodeResponseType(bytes, Enum):
5959
CIRCLE_PLUS = b"00DD" # ack for CirclePlusAllowJoiningRequest with state false
6060
CLOCK_ACCEPTED = b"00D7"
6161
JOIN_ACCEPTED = b"00D9" # ack for CirclePlusAllowJoiningRequest with state true
62+
NODE_RESET_ACK = b"00F2"
6263
POWER_LOG_INTERVAL_ACCEPTED = b"00F8" # ack for CircleMeasureIntervalRequest
64+
REAL_TIME_CLOCK_ACCEPTED = b"00DF"
65+
REAL_TIME_CLOCK_FAILED = b"00E7"
6366
RELAY_SWITCHED_OFF = b"00DE"
6467
RELAY_SWITCHED_ON = b"00D8"
6568
RELAY_SWITCH_FAILED = b"00E2"
6669
SED_CONFIG_ACCEPTED = b"00F6"
67-
REAL_TIME_CLOCK_ACCEPTED = b"00DF"
68-
REAL_TIME_CLOCK_FAILED = b"00E7"
6970

7071
# TODO: Validate these response types
7172
SED_CONFIG_FAILED = b"00F7"

plugwise_usb/network/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,17 @@ async def clear_cache(self) -> None:
166166

167167
async def unregister_node(self, mac: str) -> None:
168168
"""Unregister node from current Plugwise network."""
169+
if not validate_mac(mac):
170+
raise NodeError(f"MAC {mac} invalid")
171+
172+
node_to_remove = self._nodes[mac]
169173
try:
170-
await self._register.unregister_node(mac)
171-
except (KeyError, NodeError) as exc:
172-
raise MessageError("Mac not registered, already deleted?") from exc
174+
await self._register.unregister_node(node_to_remove)
175+
except NodeError as exc:
176+
# Preserve precise failure cause from registry/reset/remove.
177+
raise MessageError(str(exc)) from exc
178+
except KeyError as exc:
179+
raise MessageError(f"Mac {mac} not registered, already deleted?") from exc
173180

174181
await self._nodes[mac].unload()
175182
self._nodes.pop(mac)

plugwise_usb/network/registry.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logging
99
from typing import Final
1010

11-
from ..api import NodeType
11+
from ..api import NodeType, PlugwiseNode
1212
from ..constants import UTF8
1313
from ..exceptions import CacheError, NodeError, StickError
1414
from ..helpers.util import validate_mac
@@ -266,27 +266,27 @@ async def register_node(self, mac: str) -> None:
266266
if self.update_network_registration(mac):
267267
await self._exec_node_discover_callback(mac, None, False)
268268

269-
async def unregister_node(self, mac: str) -> None:
269+
async def unregister_node(self, node: PlugwiseNode) -> None:
270270
"""Unregister node from current Plugwise network."""
271-
if not validate_mac(mac):
272-
raise NodeError(f"MAC {mac} invalid")
273-
274-
if mac not in self._registry:
275-
raise NodeError(f"No existing Node ({mac}) found to unregister")
271+
if node.mac not in self._registry:
272+
raise NodeError(f"No existing Node ({node.mac}) found to unregister")
276273

277-
request = NodeRemoveRequest(self._send_to_controller, self._mac_nc, mac)
274+
# First remove the node, when succesful then reset it.
275+
request = NodeRemoveRequest(self._send_to_controller, self._mac_nc, node.mac)
278276
if (response := await request.send()) is None:
279277
raise NodeError(
280278
f"The Zigbee network coordinator '{self._mac_nc!r}'"
281-
+ f" did not respond to unregister node '{mac}'"
279+
+ f" did not respond to unregister node '{node.mac}'"
282280
)
283281
if response.status.value != 1:
284282
raise NodeError(
285283
f"The Zigbee network coordinator '{self._mac_nc!r}'"
286-
+ f" failed to unregister node '{mac}'"
284+
+ f" failed to unregister node '{node.mac}'"
287285
)
288286

289-
await self.remove_network_registration(mac)
287+
await node.reset_node()
288+
await self.remove_network_registration(node.mac)
289+
await node.clear_cache()
290290

291291
async def clear_register_cache(self) -> None:
292292
"""Clear current cache."""

plugwise_usb/nodes/node.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
from ..constants import SUPPRESS_INITIALIZATION_WARNINGS, TYPE_MODEL, UTF8
3333
from ..exceptions import FeatureError, NodeError
3434
from ..helpers.util import version_to_model
35-
from ..messages.requests import NodeInfoRequest, NodePingRequest
36-
from ..messages.responses import NodeInfoResponse, NodePingResponse
35+
from ..messages.requests import NodeInfoRequest, NodePingRequest, NodeResetRequest
36+
from ..messages.responses import NodeInfoResponse, NodePingResponse, NodeResponseType
3737
from .helpers import raise_not_loaded
3838
from .helpers.cache import NodeCache
3939
from .helpers.firmware import FEATURE_SUPPORTED_AT_FIRMWARE, SupportedVersions
@@ -391,6 +391,7 @@ async def _load_cache_file(self) -> bool:
391391
async def clear_cache(self) -> None:
392392
"""Clear current cache."""
393393
if self._node_cache is not None:
394+
_LOGGER.debug("Removing node % cache", self._mac_in_str)
394395
await self._node_cache.clear_cache()
395396

396397
async def _load_from_cache(self) -> bool:
@@ -641,6 +642,20 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any
641642

642643
return states
643644

645+
async def reset_node(self) -> None:
646+
"""Reset node present in the current Plugwise network."""
647+
timeout = 4
648+
request = NodeResetRequest(
649+
self._send, self._mac_in_bytes, self.node_type.value, timeout
650+
)
651+
response = await request.send()
652+
if response is None or response.ack_id != NodeResponseType.NODE_RESET_ACK:
653+
_LOGGER.warning(
654+
"Node %s reset not acknowledged (response=%s)",
655+
self._mac_in_str,
656+
getattr(response, "ack_id", None),
657+
)
658+
644659
async def unload(self) -> None:
645660
"""Deactivate and unload node features."""
646661
if not self._cache_enabled:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plugwise_usb"
7-
version = "0.46.0"
7+
version = "0.46.1a1"
88
license = "MIT"
99
keywords = ["home", "automation", "plugwise", "module", "usb"]
1010
classifiers = [

tests/stick_test_data.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,19 @@
13441344
+ b"00000444"
13451345
+ b"00044020",
13461346
),
1347+
b"\x05\x05\x03\x030009333333333333333302047290\r\n": (
1348+
"Reset node request for 3333333333333333",
1349+
b"000000C1", # Success ack
1350+
b"0000" + b"00F2" + b"3333333333333333", # msg_id, reset_ack, mac
1351+
),
1352+
b"\x05\x05\x03\x03001C009876543210123433333333333333331E2D\r\n": (
1353+
"Remove node request for 3333333333333333",
1354+
b"000000C1", # Success ack
1355+
b"001D" # msg_id
1356+
+ b"0098765432101234" # Circle + mac
1357+
+ b"3333333333333333"
1358+
+ b"01", # status
1359+
),
13471360
}
13481361

13491362

tests/test_usb.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,11 @@ async def test_node_discovery(self, monkeypatch: pytest.MonkeyPatch) -> None:
719719
assert stick.joined_nodes == 9
720720
assert stick.nodes.get("0098765432101234") is not None
721721
assert len(stick.nodes) == 7 # Discovered nodes
722+
723+
# Test unregistering of node
724+
await stick.unregister_node("3333333333333333")
725+
assert stick.nodes.get("3333333333333333") is None
726+
assert len(stick.nodes) == 6
722727
await stick.disconnect()
723728

724729
async def node_relay_state(

0 commit comments

Comments
 (0)