diff --git a/CHANGELOG.md b/CHANGELOG.md index e88e95631..a43a5f0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index ebbf8e587..8870a24fd 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -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) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 52bdafb96..b1777fe56 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -41,6 +41,7 @@ class NodeFeature(str, Enum): AVAILABLE = "available" BATTERY = "battery" + CIRCLEPLUS = "circleplus" ENERGY = "energy" HUMIDITY = "humidity" INFO = "info" @@ -83,6 +84,7 @@ class NodeType(Enum): NodeFeature.TEMPERATURE, NodeFeature.SENSE, NodeFeature.SWITCH, + NodeFeature.CIRCLEPLUS, ) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 948089326..877bbaf1d 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -41,7 +41,6 @@ class StickNetwork: """USB-Stick zigbee network class.""" - accept_join_request = False _event_subscriptions: dict[StickEvent, int] = {} def __init__( @@ -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: @@ -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) diff --git a/plugwise_usb/nodes/circle_plus.py b/plugwise_usb/nodes/circle_plus.py index 4de82fd25..4a2a5ac24 100644 --- a/plugwise_usb/nodes/circle_plus.py +++ b/plugwise_usb/nodes/circle_plus.py @@ -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__) @@ -37,6 +38,7 @@ async def load(self) -> bool: NodeFeature.RELAY_LOCK, NodeFeature.ENERGY, NodeFeature.POWER, + NodeFeature.CIRCLEPLUS, ), ) if await self.initialize(): @@ -73,6 +75,7 @@ async def load(self) -> bool: NodeFeature.RELAY_LOCK, NodeFeature.ENERGY, NodeFeature.POWER, + NodeFeature.CIRCLEPLUS, ), ) if not await self.initialize(): @@ -121,3 +124,18 @@ async def clock_synchronize(self) -> bool: 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 + + # JOIN_ACCEPTED is the ACK for enable=True + return NodeResponseType(response.ack_id) == NodeResponseType.JOIN_ACCEPTED diff --git a/plugwise_usb/nodes/helpers/firmware.py b/plugwise_usb/nodes/helpers/firmware.py index a0298295d..c2df3b4d0 100644 --- a/plugwise_usb/nodes/helpers/firmware.py +++ b/plugwise_usb/nodes/helpers/firmware.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 35532dce9..bcb1c4b06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/tests/test_usb.py b/tests/test_usb.py index be3241524..0631b7983 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -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): @@ -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 @@ -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: @@ -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, ) )