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 @@ 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})"
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 @@ 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 = SwitchGroup()

async def load(self) -> bool:
"""Load and activate Switch node features."""
Expand Down Expand Up @@ -73,7 +71,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,),
)
Expand All @@ -92,50 +90,40 @@ 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.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(
self._switch, state=switch_state, group=switch_group, timestamp=timestamp
)

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

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