From 753b93f34e5df574c4a03940c96b2e7ea939c1b1 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 6 Jul 2025 19:05:02 +0200 Subject: [PATCH 1/5] Update switch to return SwitchGroup object conforming to other node implementations. Update switch to publish switch group number --- plugwise_usb/api.py | 15 +++++- plugwise_usb/messages/responses.py | 5 ++ plugwise_usb/nodes/helpers/firmware.py | 3 +- plugwise_usb/nodes/node.py | 2 +- plugwise_usb/nodes/switch.py | 66 +++++++++++++------------- tests/test_usb.py | 12 +++-- 6 files changed, 62 insertions(+), 41 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 528617978..4e5436f80 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -53,7 +53,8 @@ class NodeFeature(str, Enum): RELAY = "relay" RELAY_INIT = "relay_init" RELAY_LOCK = "relay_lock" - SWITCH = "switch" + SWITCH_GROUP_1 = "switch_group_1" + SWITCH_GROUP_2 = "switch_group_2" SENSE = "sense" TEMPERATURE = "temperature" @@ -86,7 +87,8 @@ class NodeType(Enum): NodeFeature.MOTION_CONFIG, NodeFeature.TEMPERATURE, NodeFeature.SENSE, - NodeFeature.SWITCH, + NodeFeature.SWITCH_GROUP_1, + NodeFeature.SWITCH_GROUP_2, ) @@ -205,6 +207,15 @@ class RelayState: timestamp: datetime | None = None +@dataclass() +class SwitchGroup: + """Status of Switch.""" + + state: bool | None = None + group: int | None = None + timestamp: datetime | None = None + + @dataclass(frozen=True) class MotionState: """Status of motion sensor.""" diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 52bf1a7e5..1f4f1923d 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -858,6 +858,11 @@ def switch_state(self) -> bool: """Return state of switch (True = On, False = Off).""" return self._power_state.value != 0 + @property + def switch_group(self) -> int: + """Return group number.""" + return self.group.value + def __repr__(self) -> str: """Convert request into writable str.""" return f"{super().__repr__()[:-1]}, power_state={self._power_state.value}, group={self.group.value})" diff --git a/plugwise_usb/nodes/helpers/firmware.py b/plugwise_usb/nodes/helpers/firmware.py index 1379b04c6..10479feea 100644 --- a/plugwise_usb/nodes/helpers/firmware.py +++ b/plugwise_usb/nodes/helpers/firmware.py @@ -168,7 +168,8 @@ class SupportedVersions(NamedTuple): NodeFeature.RELAY_LOCK: 2.0, NodeFeature.MOTION: 2.0, NodeFeature.MOTION_CONFIG: 2.0, - NodeFeature.SWITCH: 2.0, + NodeFeature.SWITCH_GROUP_1: 2.0, + NodeFeature.SWITCH_GROUP_2: 2.0, } # endregion diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index bde5d949c..a874b65d8 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -315,7 +315,7 @@ def relay_lock(self) -> RelayLock: @raise_not_loaded def switch(self) -> bool: """Switch button value.""" - if NodeFeature.SWITCH not in self._features: + if NodeFeature.SWITCH_GROUP_1 not in self._features: raise FeatureError(f"Switch value is not supported for node {self.mac}") raise NotImplementedError() diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index b1c6d4f18..60056443f 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -6,9 +6,9 @@ from collections.abc import Awaitable, Callable from datetime import datetime import logging -from typing import Any, Final +from typing import Any -from ..api import NodeEvent, NodeFeature +from ..api import NodeEvent, NodeFeature, SwitchGroup from ..connection import StickController from ..exceptions import MessageError, NodeError from ..messages.responses import ( @@ -22,9 +22,6 @@ _LOGGER = logging.getLogger(__name__) -CACHE_SWITCH_STATE: Final = "switch_state" -CACHE_SWITCH_TIMESTAMP: Final = "switch_timestamp" - class PlugwiseSwitch(NodeSED): """Plugwise Switch node.""" @@ -39,7 +36,8 @@ def __init__( """Initialize Scan Device.""" super().__init__(mac, address, controller, loaded_callback) self._switch_subscription: Callable[[], None] | None = None - self._switch_state: bool | None = None + self._switch_group_1 = SwitchGroup() + self._switch_group_2 = SwitchGroup() async def load(self) -> bool: """Load and activate Switch node features.""" @@ -57,7 +55,8 @@ async def load(self) -> bool: NodeFeature.BATTERY, NodeFeature.INFO, NodeFeature.PING, - NodeFeature.SWITCH, + NodeFeature.SWITCH_GROUP_1, + NodeFeature.SWITCH_GROUP_2, ), ) if await self.initialize(): @@ -73,7 +72,7 @@ async def initialize(self) -> bool: return True self._switch_subscription = await self._message_subscribe( - self._switch_group, + self._switch_response, self._mac_in_bytes, (NODE_SWITCH_GROUP_ID,), ) @@ -92,11 +91,11 @@ async def unload(self) -> None: @raise_not_loaded def switch(self) -> bool: """Current state of switch.""" - return bool(self._switch_state) + return bool(self._switch_group_1.state) # endregion - async def _switch_group(self, response: PlugwiseResponse) -> bool: + async def _switch_response(self, response: PlugwiseResponse) -> bool: """Switch group request from Switch.""" if not isinstance(response, NodeSwitchGroupResponse): raise MessageError( @@ -104,37 +103,36 @@ async def _switch_group(self, response: PlugwiseResponse) -> bool: ) await gather( self._available_update_state(True, response.timestamp), - self._switch_state_update(response.switch_state, response.timestamp), + self._switch_state_update( + response.switch_state, response.switch_group, response.timestamp + ), ) return True async def _switch_state_update( - self, switch_state: bool, timestamp: datetime + self, switch_state: bool, switch_group: int, timestamp: datetime ) -> None: """Process switch state update.""" _LOGGER.debug( - "_switch_state_update for %s: %s -> %s", + "_switch_state_update for %s: %s", self.name, - self._switch_state, switch_state, ) - state_update = False - # Update cache - self._set_cache(CACHE_SWITCH_STATE, str(switch_state)) - # Check for a state change - if self._switch_state != switch_state: - self._switch_state = switch_state - state_update = True - - self._set_cache(CACHE_SWITCH_TIMESTAMP, timestamp) - if state_update: - await gather( - *[ - self.publish_feature_update_to_subscribers( - NodeFeature.SWITCH, self._switch_state - ), - self.save_cache(), - ] + if switch_group == 1: + self._switch_group_1.state = switch_state + self._switch_group_1.group = switch_group + self._switch_group_1.timestampe = timestamp + + await self.publish_feature_update_to_subscribers( + NodeFeature.SWITCH_GROUP_1, self._switch_group_1 + ) + elif switch_group == 2: + self._switch_group_2.state = switch_state + self._switch_group_2.group = switch_group + self._switch_group_2.timestampe = timestamp + + await self.publish_feature_update_to_subscribers( + NodeFeature.SWITCH_GROUP_2, self._switch_group_2 ) @raise_not_loaded @@ -154,8 +152,10 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any ) match feature: - case NodeFeature.SWITCH: - states[NodeFeature.SWITCH] = self._switch_state + case NodeFeature.SWITCH_GROUP_1: + states[NodeFeature.SWITCH_GROUP_1] = self._switch_group_1 + case NodeFeature.SWITCH_GROUP_2: + states[NodeFeature.SWITCH_GROUP_2] = self._switch_group_2 case _: state_result = await super().get_state((feature,)) states[feature] = state_result[feature] diff --git a/tests/test_usb.py b/tests/test_usb.py index 04063deb3..a611e4b76 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2363,7 +2363,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, pw_api.NodeFeature.PING, - pw_api.NodeFeature.SWITCH, + pw_api.NodeFeature.SWITCH_GROUP_1, + pw_api.NodeFeature.SWITCH_GROUP_2, ) ) @@ -2372,7 +2373,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign pw_api.NodeFeature.AVAILABLE, pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, - pw_api.NodeFeature.SWITCH, + pw_api.NodeFeature.SWITCH_GROUP_1, + pw_api.NodeFeature.SWITCH_GROUP_2, ) ) assert not state[pw_api.NodeFeature.AVAILABLE].state @@ -2633,7 +2635,8 @@ async def test_node_discovery_and_load( # noqa: PLR0915 pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, pw_api.NodeFeature.PING, - pw_api.NodeFeature.SWITCH, + pw_api.NodeFeature.SWITCH_GROUP_1, + pw_api.NodeFeature.SWITCH_GROUP_2, ) ) state = await stick.nodes["8888888888888888"].get_state( @@ -2641,7 +2644,8 @@ async def test_node_discovery_and_load( # noqa: PLR0915 pw_api.NodeFeature.AVAILABLE, pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, - pw_api.NodeFeature.SWITCH, + pw_api.NodeFeature.SWITCH_GROUP_1, + pw_api.NodeFeature.SWITCH_GROUP_2, ) ) assert state[pw_api.NodeFeature.AVAILABLE].state From d93069151584a2285d7d7061964c30a29bf9e68c Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 8 Jul 2025 08:41:28 +0200 Subject: [PATCH 2/5] simply again, separation between state and group is not for the module solve --- plugwise_usb/api.py | 6 ++--- plugwise_usb/nodes/helpers/firmware.py | 3 +-- plugwise_usb/nodes/node.py | 2 +- plugwise_usb/nodes/switch.py | 35 ++++++++------------------ tests/test_usb.py | 12 +++------ 5 files changed, 19 insertions(+), 39 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 4e5436f80..b6b663aea 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -53,8 +53,7 @@ class NodeFeature(str, Enum): RELAY = "relay" RELAY_INIT = "relay_init" RELAY_LOCK = "relay_lock" - SWITCH_GROUP_1 = "switch_group_1" - SWITCH_GROUP_2 = "switch_group_2" + SWITCH = "switch" SENSE = "sense" TEMPERATURE = "temperature" @@ -87,8 +86,7 @@ class NodeType(Enum): NodeFeature.MOTION_CONFIG, NodeFeature.TEMPERATURE, NodeFeature.SENSE, - NodeFeature.SWITCH_GROUP_1, - NodeFeature.SWITCH_GROUP_2, + NodeFeature.SWITCH, ) diff --git a/plugwise_usb/nodes/helpers/firmware.py b/plugwise_usb/nodes/helpers/firmware.py index 10479feea..1379b04c6 100644 --- a/plugwise_usb/nodes/helpers/firmware.py +++ b/plugwise_usb/nodes/helpers/firmware.py @@ -168,8 +168,7 @@ class SupportedVersions(NamedTuple): NodeFeature.RELAY_LOCK: 2.0, NodeFeature.MOTION: 2.0, NodeFeature.MOTION_CONFIG: 2.0, - NodeFeature.SWITCH_GROUP_1: 2.0, - NodeFeature.SWITCH_GROUP_2: 2.0, + NodeFeature.SWITCH: 2.0, } # endregion diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index a874b65d8..bde5d949c 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -315,7 +315,7 @@ def relay_lock(self) -> RelayLock: @raise_not_loaded def switch(self) -> bool: """Switch button value.""" - if NodeFeature.SWITCH_GROUP_1 not in self._features: + if NodeFeature.SWITCH not in self._features: raise FeatureError(f"Switch value is not supported for node {self.mac}") raise NotImplementedError() diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index 60056443f..33aed8879 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -36,8 +36,7 @@ def __init__( """Initialize Scan Device.""" super().__init__(mac, address, controller, loaded_callback) self._switch_subscription: Callable[[], None] | None = None - self._switch_group_1 = SwitchGroup() - self._switch_group_2 = SwitchGroup() + self._switch = SwitchGroup() async def load(self) -> bool: """Load and activate Switch node features.""" @@ -55,8 +54,7 @@ async def load(self) -> bool: NodeFeature.BATTERY, NodeFeature.INFO, NodeFeature.PING, - NodeFeature.SWITCH_GROUP_1, - NodeFeature.SWITCH_GROUP_2, + NodeFeature.SWITCH, ), ) if await self.initialize(): @@ -91,7 +89,7 @@ async def unload(self) -> None: @raise_not_loaded def switch(self) -> bool: """Current state of switch.""" - return bool(self._switch_group_1.state) + return bool(self._switch.state) # endregion @@ -118,22 +116,13 @@ async def _switch_state_update( self.name, switch_state, ) - if switch_group == 1: - self._switch_group_1.state = switch_state - self._switch_group_1.group = switch_group - self._switch_group_1.timestampe = timestamp + self._switch.state = switch_state + self._switch.group = switch_group + self._switch.timestampe = timestamp - await self.publish_feature_update_to_subscribers( - NodeFeature.SWITCH_GROUP_1, self._switch_group_1 - ) - elif switch_group == 2: - self._switch_group_2.state = switch_state - self._switch_group_2.group = switch_group - self._switch_group_2.timestampe = timestamp - - await self.publish_feature_update_to_subscribers( - NodeFeature.SWITCH_GROUP_2, self._switch_group_2 - ) + await self.publish_feature_update_to_subscribers( + NodeFeature.SWITCH, self._switch + ) @raise_not_loaded async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any]: @@ -152,10 +141,8 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any ) match feature: - case NodeFeature.SWITCH_GROUP_1: - states[NodeFeature.SWITCH_GROUP_1] = self._switch_group_1 - case NodeFeature.SWITCH_GROUP_2: - states[NodeFeature.SWITCH_GROUP_2] = self._switch_group_2 + case NodeFeature.SWITCH: + states[NodeFeature.SWITCH] = self._switch case _: state_result = await super().get_state((feature,)) states[feature] = state_result[feature] diff --git a/tests/test_usb.py b/tests/test_usb.py index a611e4b76..04063deb3 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2363,8 +2363,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, pw_api.NodeFeature.PING, - pw_api.NodeFeature.SWITCH_GROUP_1, - pw_api.NodeFeature.SWITCH_GROUP_2, + pw_api.NodeFeature.SWITCH, ) ) @@ -2373,8 +2372,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign pw_api.NodeFeature.AVAILABLE, pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, - pw_api.NodeFeature.SWITCH_GROUP_1, - pw_api.NodeFeature.SWITCH_GROUP_2, + pw_api.NodeFeature.SWITCH, ) ) assert not state[pw_api.NodeFeature.AVAILABLE].state @@ -2635,8 +2633,7 @@ async def test_node_discovery_and_load( # noqa: PLR0915 pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, pw_api.NodeFeature.PING, - pw_api.NodeFeature.SWITCH_GROUP_1, - pw_api.NodeFeature.SWITCH_GROUP_2, + pw_api.NodeFeature.SWITCH, ) ) state = await stick.nodes["8888888888888888"].get_state( @@ -2644,8 +2641,7 @@ async def test_node_discovery_and_load( # noqa: PLR0915 pw_api.NodeFeature.AVAILABLE, pw_api.NodeFeature.BATTERY, pw_api.NodeFeature.INFO, - pw_api.NodeFeature.SWITCH_GROUP_1, - pw_api.NodeFeature.SWITCH_GROUP_2, + pw_api.NodeFeature.SWITCH, ) ) assert state[pw_api.NodeFeature.AVAILABLE].state From 6b203f19eb45b1f1a61447ffa83042dd6ae1955a Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 8 Jul 2025 10:56:31 +0200 Subject: [PATCH 3/5] implement coderabitai suggestion --- plugwise_usb/api.py | 2 +- plugwise_usb/nodes/switch.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index b6b663aea..c5c20afa1 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -205,7 +205,7 @@ class RelayState: timestamp: datetime | None = None -@dataclass() +@dataclass(frozen=True) class SwitchGroup: """Status of Switch.""" diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index 33aed8879..a0f0edec2 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -4,6 +4,7 @@ from asyncio import gather from collections.abc import Awaitable, Callable +from dataclasses import replace from datetime import datetime import logging from typing import Any @@ -116,9 +117,9 @@ async def _switch_state_update( self.name, switch_state, ) - self._switch.state = switch_state - self._switch.group = switch_group - self._switch.timestampe = timestamp + self._switch = replace( + self._switch, state=switch_state, group=switch_group, timestamp=timestamp + ) await self.publish_feature_update_to_subscribers( NodeFeature.SWITCH, self._switch From 9ca4bb9b65c5a6ff56366d2b101c8f83e2217993 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 8 Jul 2025 11:11:49 +0200 Subject: [PATCH 4/5] update changelog --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2c5ae44..ea852f188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.44.7 - 2025-07-08 + +- PR [282](https://github.com/plugwise/python-plugwise-usb/pull/282): Finalize switch implementation + ## v0.44.6 - 2025-07-06 - PR [279](https://github.com/plugwise/python-plugwise-usb/pull/279): Improve registry cache and node load behaviour diff --git a/pyproject.toml b/pyproject.toml index 3a0040b57..ff8bc48cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.6" +version = "0.44.7" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 91e75a883d19838da5e51217803cb800d43ddb88 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 8 Jul 2025 11:30:30 +0200 Subject: [PATCH 5/5] update title --- plugwise_usb/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index c5c20afa1..cb6f6e171 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -207,7 +207,7 @@ class RelayState: @dataclass(frozen=True) class SwitchGroup: - """Status of Switch.""" + """Status and Group of Switch.""" state: bool | None = None group: int | None = None