Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d6416dd
Add NODE_RESET_ACK NodeResponseType
bouwew Sep 10, 2025
d41b2ec
Update NodeResetResponse info
bouwew Sep 10, 2025
803a09e
Add reset_node() function
bouwew Sep 10, 2025
960df6f
Add reset_node() function description to PlugwiseNode protocol
bouwew Sep 11, 2025
328c52f
Update unregister_node functions
bouwew Sep 11, 2025
d98d104
Add missing await
bouwew Sep 11, 2025
5ffcfda
Try
bouwew Sep 11, 2025
30f6702
Add unregister_node() to api
bouwew Sep 11, 2025
44757c4
Try 2
bouwew Sep 11, 2025
e1fa386
Add reset_node request plus response
bouwew Sep 13, 2025
ca0fc39
Try 3
bouwew Sep 13, 2025
6598864
Correct message
bouwew Sep 13, 2025
77af8e9
Try 4
bouwew Sep 13, 2025
a33aaf4
Line up mac
bouwew Sep 13, 2025
52109b8
Update/correct NodeResetRequest
bouwew Sep 13, 2025
18fe4ad
NodeReset requires _mac_in_bytes
bouwew Sep 13, 2025
3a0887a
Add node-remove messages
bouwew Sep 13, 2025
5fe0822
Implement CR-suggestions
bouwew Sep 13, 2025
b47b3ce
Change NodeType to IntEnum
bouwew Sep 13, 2025
2bda229
Write node_reset_response in one line
bouwew Sep 13, 2025
de15fb1
Update node_reset message with CRC
bouwew Sep 13, 2025
a4d0a12
Update node_remove message with CRC
bouwew Sep 13, 2025
d326a1a
Use self.node_type.value to get to int-type
bouwew Sep 13, 2025
6b25651
Ruffed
bouwew Sep 13, 2025
96c3f89
Delete the node-cache of the deleted Node
bouwew Sep 13, 2025
3b67cd1
Revert test-script change
bouwew Sep 13, 2025
99ffc8b
Revert adding unregister_node() to api
bouwew Sep 13, 2025
0f0ac9f
Remove test-debug-messages
bouwew Sep 13, 2025
e74a7cd
Revert to original response.ack_id
bouwew Sep 13, 2025
29f7701
Remove commented NODE_RESET_NACK
bouwew Sep 13, 2025
403a912
Implements CR suggestions
bouwew Sep 13, 2025
04f1a41
Update CHANGELOG
bouwew Sep 13, 2025
824f60a
Bump to v0.46.1a0 for testing
bouwew Sep 13, 2025
104521a
Correct varname
bouwew Sep 13, 2025
e851ff9
First remove, then reset
bouwew Sep 13, 2025
d71a3fe
Bump to a1
bouwew Sep 13, 2025
4a95fe8
Add debug-logger for node-clear_cache
bouwew Sep 14, 2025
a9238b3
Revert NodeType back to
bouwew Sep 14, 2025
dec9242
Update CHANGELOG
bouwew Sep 14, 2025
c9ffe52
Update CHANGELOG
bouwew Sep 15, 2025
e55e1e8
Improve CHANGELOG
bouwew Sep 16, 2025
f06b67b
Clean up
bouwew Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Ongoing

- 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.
- PR [342](https://github.com/plugwise/python-plugwise-usb/pull/342): Improve node_type chaching.

## 0.46.0 - 2025-09-12
Expand Down
19 changes: 8 additions & 11 deletions plugwise_usb/messages/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,11 +481,10 @@ class NodeResetRequest(PlugwiseRequest):
"""TODO:Some kind of reset request.

Supported protocols : 1.0, 2.0, 2.1
Response message : <UNKNOWN>
Response message : NodeResponse with NODE_RESET_ACK/NACK (@dirixmjm & @bouwew 20250910)
"""

_identifier = b"0009"
_reply_identifier = b"0003"

def __init__(
self,
Expand All @@ -496,20 +495,18 @@ def __init__(
) -> None:
"""Initialize NodeResetRequest message object."""
super().__init__(send_fn, mac)
self._args += [
Int(moduletype, length=2),
Int(timeout, length=2),
]
module_id = getattr(moduletype, "value", moduletype)
self._args += [Int(module_id, length=2), Int(timeout, length=2)]

async def send(self) -> NodeSpecificResponse | None:
async def send(self) -> NodeResponse | None:
"""Send request."""
result = await self._send_request()
if isinstance(result, NodeSpecificResponse):
if isinstance(result, NodeResponse):
return result
if result is None:
return None
raise MessageError(
f"Invalid response message. Received {result.__class__.__name__}, expected NodeSpecificResponse"
f"Invalid response message. Received {result.__class__.__name__}, expected NodeResponse"
)


Expand Down Expand Up @@ -863,11 +860,11 @@ def __init__(
self,
send_fn: Callable[[PlugwiseRequest, bool], Awaitable[PlugwiseResponse | None]],
mac_circle_plus: bytes,
mac_to_unjoined: str,
mac_to_unjoin: str,
) -> None:
"""Initialize NodeRemoveRequest message object."""
super().__init__(send_fn, mac_circle_plus)
self._args.append(String(mac_to_unjoined, length=16))
self._args.append(String(mac_to_unjoin, length=16))

async def send(self) -> NodeRemoveResponse | None:
"""Send request."""
Expand Down
5 changes: 3 additions & 2 deletions plugwise_usb/messages/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ class NodeResponseType(bytes, Enum):
CIRCLE_PLUS = b"00DD" # ack for CirclePlusAllowJoiningRequest with state false
CLOCK_ACCEPTED = b"00D7"
JOIN_ACCEPTED = b"00D9" # ack for CirclePlusAllowJoiningRequest with state true
NODE_RESET_ACK = b"00F2"
POWER_LOG_INTERVAL_ACCEPTED = b"00F8" # ack for CircleMeasureIntervalRequest
REAL_TIME_CLOCK_ACCEPTED = b"00DF"
REAL_TIME_CLOCK_FAILED = b"00E7"
RELAY_SWITCHED_OFF = b"00DE"
RELAY_SWITCHED_ON = b"00D8"
RELAY_SWITCH_FAILED = b"00E2"
SED_CONFIG_ACCEPTED = b"00F6"
REAL_TIME_CLOCK_ACCEPTED = b"00DF"
REAL_TIME_CLOCK_FAILED = b"00E7"

# TODO: Validate these response types
SED_CONFIG_FAILED = b"00F7"
Expand Down
13 changes: 10 additions & 3 deletions plugwise_usb/network/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,17 @@ async def clear_cache(self) -> None:

async def unregister_node(self, mac: str) -> None:
"""Unregister node from current Plugwise network."""
if not validate_mac(mac):
raise NodeError(f"MAC {mac} invalid")

node_to_remove = self._nodes[mac]
try:
await self._register.unregister_node(mac)
except (KeyError, NodeError) as exc:
raise MessageError("Mac not registered, already deleted?") from exc
await self._register.unregister_node(node_to_remove)
except NodeError as exc:
# Preserve precise failure cause from registry/reset/remove.
raise MessageError(str(exc)) from exc
except KeyError as exc:
raise MessageError(f"Mac {mac} not registered, already deleted?") from exc

await self._nodes[mac].unload()
self._nodes.pop(mac)
Expand Down
22 changes: 11 additions & 11 deletions plugwise_usb/network/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import logging
from typing import Final

from ..api import NodeType
from ..api import NodeType, PlugwiseNode
from ..constants import UTF8
from ..exceptions import CacheError, NodeError, StickError
from ..helpers.util import validate_mac
Expand Down Expand Up @@ -266,27 +266,27 @@ async def register_node(self, mac: str) -> None:
if self.update_network_registration(mac):
await self._exec_node_discover_callback(mac, None, False)

async def unregister_node(self, mac: str) -> None:
async def unregister_node(self, node: PlugwiseNode) -> None:
"""Unregister node from current Plugwise network."""
if not validate_mac(mac):
raise NodeError(f"MAC {mac} invalid")

if mac not in self._registry:
raise NodeError(f"No existing Node ({mac}) found to unregister")
if node.mac not in self._registry:
raise NodeError(f"No existing Node ({node.mac}) found to unregister")

request = NodeRemoveRequest(self._send_to_controller, self._mac_nc, mac)
# First remove the node, when succesful then reset it.
request = NodeRemoveRequest(self._send_to_controller, self._mac_nc, node.mac)
if (response := await request.send()) is None:
raise NodeError(
f"The Zigbee network coordinator '{self._mac_nc!r}'"
+ f" did not respond to unregister node '{mac}'"
+ f" did not respond to unregister node '{node.mac}'"
)
if response.status.value != 1:
raise NodeError(
f"The Zigbee network coordinator '{self._mac_nc!r}'"
+ f" failed to unregister node '{mac}'"
+ f" failed to unregister node '{node.mac}'"
)

await self.remove_network_registration(mac)
await node.reset_node()
await self.remove_network_registration(node.mac)
await node.clear_cache()

async def clear_register_cache(self) -> None:
"""Clear current cache."""
Expand Down
19 changes: 17 additions & 2 deletions plugwise_usb/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
from ..constants import SUPPRESS_INITIALIZATION_WARNINGS, TYPE_MODEL, UTF8
from ..exceptions import FeatureError, NodeError
from ..helpers.util import version_to_model
from ..messages.requests import NodeInfoRequest, NodePingRequest
from ..messages.responses import NodeInfoResponse, NodePingResponse
from ..messages.requests import NodeInfoRequest, NodePingRequest, NodeResetRequest
from ..messages.responses import NodeInfoResponse, NodePingResponse, NodeResponseType
from .helpers import raise_not_loaded
from .helpers.cache import NodeCache
from .helpers.firmware import FEATURE_SUPPORTED_AT_FIRMWARE, SupportedVersions
Expand Down Expand Up @@ -391,6 +391,7 @@ async def _load_cache_file(self) -> bool:
async def clear_cache(self) -> None:
"""Clear current cache."""
if self._node_cache is not None:
_LOGGER.debug("Removing node % cache", self._mac_in_str)
await self._node_cache.clear_cache()

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

return states

async def reset_node(self) -> None:
"""Reset node present in the current Plugwise network."""
timeout = 4
request = NodeResetRequest(
self._send, self._mac_in_bytes, self.node_type.value, timeout
)
response = await request.send()
if response is None or response.ack_id != NodeResponseType.NODE_RESET_ACK:
_LOGGER.warning(
"Node %s reset not acknowledged (response=%s)",
self._mac_in_str,
getattr(response, "ack_id", None),
)

async def unload(self) -> None:
"""Deactivate and unload node features."""
if not self._cache_enabled:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "plugwise_usb"
version = "0.46.0"
version = "0.46.1a1"
license = "MIT"
keywords = ["home", "automation", "plugwise", "module", "usb"]
classifiers = [
Expand Down
13 changes: 13 additions & 0 deletions tests/stick_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,19 @@
+ b"00000444"
+ b"00044020",
),
b"\x05\x05\x03\x030009333333333333333302047290\r\n": (
"Reset node request for 3333333333333333",
b"000000C1", # Success ack
b"0000" + b"00F2" + b"3333333333333333", # msg_id, reset_ack, mac
),
b"\x05\x05\x03\x03001C009876543210123433333333333333331E2D\r\n": (
"Remove node request for 3333333333333333",
b"000000C1", # Success ack
b"001D" # msg_id
+ b"0098765432101234" # Circle + mac
+ b"3333333333333333"
+ b"01", # status
),
}


Expand Down
5 changes: 5 additions & 0 deletions tests/test_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,11 @@ async def test_node_discovery(self, monkeypatch: pytest.MonkeyPatch) -> None:
assert stick.joined_nodes == 9
assert stick.nodes.get("0098765432101234") is not None
assert len(stick.nodes) == 7 # Discovered nodes

# Test unregistering of node
await stick.unregister_node("3333333333333333")
assert stick.nodes.get("3333333333333333") is None
assert len(stick.nodes) == 6
await stick.disconnect()

async def node_relay_state(
Expand Down