Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v0.44.3 - 2025-06-12

- PR [#260](https://github.com/plugwise/python-plugwise-usb/pull/260)
- Expose enable-auto-joining via CirclePlus interface

## v0.44.2 - 2025-06-11

- Bugfix: implement solution for Issue [#259](https://github.com/plugwise/plugwise_usb-beta/issues/259)
Expand Down
31 changes: 0 additions & 31 deletions plugwise_usb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,37 +180,6 @@ def port(self, port: str) -> None:

self._port = port

@property
def accept_join_request(self) -> bool | None:
"""Automatically accept joining request of new nodes."""
if not self._controller.is_connected:
return None
if self._network is None or not self._network.is_running:
return None
return self._network.accept_join_request

async def set_accept_join_request(self, state: bool) -> bool:
"""Configure join request setting."""
if not self._controller.is_connected:
raise StickError(
"Cannot accept joining node"
+ " without an active USB-Stick connection."
)

if self._network is None or not self._network.is_running:
raise StickError(
"Cannot accept joining node"
+ " without node discovery be activated. Call discover() first."
)

# Observation: joining is only temporarily possible after a HA (re)start or
# Integration reload, force the setting when used otherwise
try:
await self._network.allow_join_requests(state)
except (MessageError, NodeError) as exc:
raise NodeError(f"Failed setting accept joining: {exc}") from exc
return True

async def energy_reset_request(self, mac: str) -> bool:
"""Send an energy-reset request to a Node."""
_LOGGER.debug("Resetting energy logs for %s", mac)
Expand Down
2 changes: 2 additions & 0 deletions plugwise_usb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class NodeFeature(str, Enum):

AVAILABLE = "available"
BATTERY = "battery"
CIRCLEPLUS = "circleplus"
ENERGY = "energy"
HUMIDITY = "humidity"
INFO = "info"
Expand Down Expand Up @@ -83,6 +84,7 @@ class NodeType(Enum):
NodeFeature.TEMPERATURE,
NodeFeature.SENSE,
NodeFeature.SWITCH,
NodeFeature.CIRCLEPLUS,
)


Expand Down
20 changes: 0 additions & 20 deletions plugwise_usb/network/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
class StickNetwork:
"""USB-Stick zigbee network class."""

accept_join_request = False
_event_subscriptions: dict[StickEvent, int] = {}

def __init__(
Expand Down Expand Up @@ -152,9 +151,6 @@ def registry(self) -> dict[int, tuple[str, NodeType | None]]:

async def register_node(self, mac: str) -> bool:
"""Register node to Plugwise network."""
if not self.accept_join_request:
return False

try:
await self._register.register_node(mac)
except NodeError as exc:
Expand Down Expand Up @@ -527,22 +523,6 @@ async def stop(self) -> None:

# endregion

async def allow_join_requests(self, state: bool) -> None:
"""Enable or disable Plugwise network."""
request = CirclePlusAllowJoiningRequest(self._controller.send, state)
if (response := await request.send()) is None:
raise NodeError("No response for CirclePlusAllowJoiningRequest.")

if response.response_type not in (
NodeResponseType.JOIN_ACCEPTED, NodeResponseType.CIRCLE_PLUS
):
raise MessageError(
f"Unknown NodeResponseType '{response.response_type.name}' received"
)

_LOGGER.debug("Sent AllowJoiningRequest to Circle+ with state=%s", state)
self.accept_join_request = state

async def energy_reset_request(self, mac: str) -> None:
"""Send an energy-reset to a Node."""
self._validate_energy_node(mac)
Expand Down
20 changes: 19 additions & 1 deletion plugwise_usb/nodes/circle_plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
from ..messages.requests import (
CirclePlusRealTimeClockGetRequest,
CirclePlusRealTimeClockSetRequest,
CirclePlusAllowJoiningRequest,
)
from ..messages.responses import NodeResponseType
from .circle import PlugwiseCircle
from .helpers.firmware import CIRCLE_PLUS_FIRMWARE_SUPPORT

from .helpers import raise_not_loaded
_LOGGER = logging.getLogger(__name__)


Expand All @@ -37,6 +38,7 @@
NodeFeature.RELAY_LOCK,
NodeFeature.ENERGY,
NodeFeature.POWER,
NodeFeature.CIRCLEPLUS,
),
)
if await self.initialize():
Expand Down Expand Up @@ -73,6 +75,7 @@
NodeFeature.RELAY_LOCK,
NodeFeature.ENERGY,
NodeFeature.POWER,
NodeFeature.CIRCLEPLUS,
),
)
if not await self.initialize():
Expand Down Expand Up @@ -121,3 +124,18 @@
self.name,
)
return False

@raise_not_loaded
async def enable_auto_join(self) -> bool:
"""Enable auto-join on the Circle+.

Returns:
bool: True if the request was acknowledged, False otherwise.
"""
_LOGGER.info("Enabling auto-join for CirclePlus")
request = CirclePlusAllowJoiningRequest(self._send, True)
if (response := await request.send()) is None:
return False

Check warning on line 138 in plugwise_usb/nodes/circle_plus.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/circle_plus.py#L135-L138

Added lines #L135 - L138 were not covered by tests

# JOIN_ACCEPTED is the ACK for enable=True
return NodeResponseType(response.ack_id) == NodeResponseType.JOIN_ACCEPTED

Check warning on line 141 in plugwise_usb/nodes/circle_plus.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/circle_plus.py#L141

Added line #L141 was not covered by tests
1 change: 1 addition & 0 deletions plugwise_usb/nodes/helpers/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class SupportedVersions(NamedTuple):
NodeFeature.MOTION: 2.0,
NodeFeature.MOTION_CONFIG: 2.0,
NodeFeature.SWITCH: 2.0,
NodeFeature.CIRCLEPLUS: 2.0,
}

# endregion
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.44.2"
version = "0.44.3"
license = "MIT"
keywords = ["home", "automation", "plugwise", "module", "usb"]
classifiers = [
Expand Down
66 changes: 31 additions & 35 deletions tests/test_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ async def test_msg_properties(self) -> None:
async def test_stick_connect_without_port(self) -> None:
"""Test connecting to stick without port config."""
stick = pw_stick.Stick()
assert stick.accept_join_request is None
assert stick.nodes == {}
assert stick.joined_nodes is None
with pytest.raises(pw_exceptions.StickError):
Expand Down Expand Up @@ -466,10 +465,6 @@ async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None:
assert not stick.network_discovered
assert stick.network_state
assert stick.network_id == 17185
assert stick.accept_join_request is None
# test failing of join requests without active discovery
with pytest.raises(pw_exceptions.StickError):
await stick.set_accept_join_request(True)
unsub_connect()
await stick.disconnect()
assert not stick.network_state
Expand Down Expand Up @@ -651,36 +646,36 @@ async def node_join(self, event: pw_api.NodeEvent, mac: str) -> None: # type: i
)
)

@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)
await stick.set_accept_join_request(True)
# 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"1254") # @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()

@pytest.mark.asyncio
async def test_node_discovery(self, monkeypatch: pytest.MonkeyPatch) -> None:
Expand Down Expand Up @@ -2498,6 +2493,7 @@ async def test_node_discovery_and_load(
pw_api.NodeFeature.RELAY,
pw_api.NodeFeature.RELAY_LOCK,
pw_api.NodeFeature.ENERGY,
pw_api.NodeFeature.CIRCLEPLUS,
pw_api.NodeFeature.POWER,
)
)
Expand Down
Loading