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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions plugwise_usb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ class RelayState:
timestamp: datetime | None = None


@dataclass(frozen=True)
class SwitchGroup:
"""Status and Group of Switch."""

state: bool | None = None
group: int | None = None
timestamp: datetime | None = None


@dataclass(frozen=True)
class MotionState:
"""Status of motion sensor."""
Expand Down
5 changes: 5 additions & 0 deletions plugwise_usb/messages/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,11 @@
"""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

Check warning on line 864 in plugwise_usb/messages/responses.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/messages/responses.py#L864

Added line #L864 was not covered by tests

def __repr__(self) -> str:
"""Convert request into writable str."""
return f"{super().__repr__()[:-1]}, power_state={self._power_state.value}, group={self.group.value})"
Expand Down
52 changes: 20 additions & 32 deletions plugwise_usb/nodes/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

from asyncio import gather
from collections.abc import Awaitable, Callable
from dataclasses import replace
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 (
Expand All @@ -22,9 +23,6 @@

_LOGGER = logging.getLogger(__name__)

CACHE_SWITCH_STATE: Final = "switch_state"
CACHE_SWITCH_TIMESTAMP: Final = "switch_timestamp"


class PlugwiseSwitch(NodeSED):
"""Plugwise Switch node."""
Expand All @@ -39,7 +37,7 @@
"""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 = SwitchGroup()

async def load(self) -> bool:
"""Load and activate Switch node features."""
Expand Down Expand Up @@ -73,7 +71,7 @@
return True

self._switch_subscription = await self._message_subscribe(
self._switch_group,
self._switch_response,
self._mac_in_bytes,
(NODE_SWITCH_GROUP_ID,),
)
Expand All @@ -92,50 +90,40 @@
@raise_not_loaded
def switch(self) -> bool:
"""Current state of switch."""
return bool(self._switch_state)
return bool(self._switch.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(
f"Invalid response message type ({response.__class__.__name__}) received, expected NodeSwitchGroupResponse"
)
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(),
]
)
self._switch = replace(

Check warning on line 120 in plugwise_usb/nodes/switch.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/switch.py#L120

Added line #L120 was not covered by tests
self._switch, state=switch_state, group=switch_group, timestamp=timestamp
)

await self.publish_feature_update_to_subscribers(

Check warning on line 124 in plugwise_usb/nodes/switch.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/switch.py#L124

Added line #L124 was not covered by tests
NodeFeature.SWITCH, self._switch
)

@raise_not_loaded
async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any]:
Expand All @@ -155,7 +143,7 @@

match feature:
case NodeFeature.SWITCH:
states[NodeFeature.SWITCH] = self._switch_state
states[NodeFeature.SWITCH] = self._switch
case _:
state_result = await super().get_state((feature,))
states[feature] = state_result[feature]
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.44.6"
version = "0.44.7"
license = "MIT"
keywords = ["home", "automation", "plugwise", "module", "usb"]
classifiers = [
Expand Down
Loading