diff --git a/CHANGELOG.md b/CHANGELOG.md index c05fde6b4..4d9bb8190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.47.6 - 2026-03-11 + +PR [425](https://github.com/plugwise/python-plugwise-usb/pull/425): More 0138-related improvements + ## v0.47.5 - 2026-03-09 PR [424](https://github.com/plugwise/python-plugwise-usb/pull/424): Fix 0138 AckResponse diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 63251f11d..26f5a37ae 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -26,6 +26,7 @@ CirclePlusRealTimeClockResponse, CirclePlusScanResponse, CirclePowerUsageResponse, + CircleRelayInitStateResponse, EnergyCalibrationResponse, NodeAckResponse, NodeFeaturesResponse, @@ -60,6 +61,7 @@ class PlugwiseRequest(PlugwiseMessage): """Base class for request messages to be sent from by USB-Stick.""" _reply_identifier: bytes | None = b"0000" + _reply_identifier_2: bytes | None = None def __init__( self, @@ -157,7 +159,7 @@ async def subscribe_to_response( [ Callable[[PlugwiseResponse], Coroutine[Any, Any, bool]], bytes | None, - tuple[bytes | None, ...] | None, + tuple[bytes, ...] | None, bytes | None, ], Coroutine[Any, Any, Callable[[], None]], @@ -171,10 +173,18 @@ async def subscribe_to_response( self._unsubscribe_stick_response = await stick_subscription_fn( self._process_stick_response, self._seq_id, None ) + reply_identifiers = ( + tuple( + reply_id + for reply_id in (self._reply_identifier, self._reply_identifier_2) + if reply_id is not None + ) + or None + ) self._unsubscribe_node_response = await node_subscription_fn( self.process_node_response, self._mac, - (self._reply_identifier,), + reply_identifiers, self._seq_id, ) @@ -1510,11 +1520,13 @@ class CircleRelayInitStateRequest(PlugwiseRequest): """Get or set initial relay state after power-up of Circle. Supported protocols : 2.6 - Response message : NodeAckResponse # CircleInitRelayStateResponse + Response message : NodeAckResponse for set + : CircleRelayInitStateResponse for get """ _identifier = b"0138" # PWCircleGetSetInitialRelaisStateRequestV2_6 - _reply_identifier = b"0100" # b"0139" # PWCircleGetSetInitialRelaisStateReplyV2_6 + _reply_identifier = b"0139" # CircleRelayInitStateResponse + _reply_identifier_2 = b"0100" # NodeAckResponse def __init__( self, @@ -1530,13 +1542,13 @@ def __init__( self.relay = Int(1 if relay_state else 0, length=2) self._args += [self.set_or_get, self.relay] - async def send(self) -> NodeAckResponse | None: + async def send(self) -> CircleRelayInitStateResponse | NodeAckResponse | None: """Send request.""" result = await self._send_request() - if isinstance(result, NodeAckResponse): + if isinstance(result, CircleRelayInitStateResponse | NodeAckResponse): return result if result is None: return None raise MessageError( - f"Invalid response message. Received {result.__class__.__name__}, expected NodeAckResponse" + f"Invalid response message. Received {result.__class__.__name__}, expected CircleRelayInitStateResponse or NodeAckResponse" ) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index beb8e6d9f..7be3cf599 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -68,19 +68,21 @@ class NodeResponseType(bytes, Enum): CIRCLE_PLUS = b"00DD" # ack for CirclePlusAllowJoiningRequest with state false - HN_ALLOWNEWNODESTOJOIN_ACK_0 RELAY_SWITCHED_OFF = b"00DE" # HN_SETRELAISPOSITION_OFF REAL_TIME_CLOCK_ACCEPTED = b"00DF" # HN_SETRTCDATA_ACK - RELAY_SWITCH_FAILED = b"00E2" # HN_SETRELAISPOSITION_DENIED + RELAY_SWITCH_FAILED = b"00E2" # HN_SETRELAISPOSITION_DENIED REAL_TIME_CLOCK_FAILED = b"00E7" # HN_SETRTCDATA_NACK NODE_RESET_ACK = b"00F2" # HN_REPLYROLECHANGED_OK NODE_RESET_FAIL = b"00F3" # HN_REPLYROLECHANGED_NOT_OK SED_CONFIG_ACCEPTED = b"00F6" # HN_ACKSETSLEEPBEHAVIOR SED_CONFIG_FAILED = b"00F7" # HN_ACKREQUESTSELFREMOVALFROMNETWORK - POWER_LOG_INTERVAL_ACCEPTED = b"00F8" # ack for CircleMeasureIntervalRequest - HN_ACKSETPOWERRECORDING + POWER_LOG_INTERVAL_ACCEPTED = ( + b"00F8" # ack for CircleMeasureIntervalRequest - HN_ACKSETPOWERRECORDING + ) class NodeAckResponseType(bytes, Enum): """Response types of a 'NodeAckResponse' reply message.""" - DEFAULT_ACK= b"00A0" # HN_DEFAULT_ACK + DEFAULT_ACK = b"00A0" # HN_DEFAULT_ACK DEFAULT_FAIL = b"00A1" # HN_DEFAULT_NACK SENSE_INTERVAL_ACCEPTED = b"00B3" # HN_ACKSETSENSEINTERVAL_ACK SENSE_INTERVAL_FAILED = b"00B4" # HN_ACKSETSENSEINTERVAL_NACK diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index c9f99fb97..2d2a5f3b9 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -46,7 +46,12 @@ EnergyCalibrationRequest, NodeInfoRequest, ) -from ..messages.responses import NodeInfoResponse, NodeResponseType +from ..messages.responses import ( + CircleRelayInitStateResponse, + NodeAckResponseType, + NodeInfoResponse, + NodeResponseType, +) from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters from .helpers.firmware import CIRCLE_FIRMWARE_SUPPORT @@ -1140,28 +1145,54 @@ async def _relay_init_get(self) -> bool | None: "Retrieval of initial state of relay is not " + f"supported for device {self.name}" ) - request = CircleRelayInitStateRequest( - self._send, self._mac_in_bytes, False, False - ) - if (response := await request.send()) is not None: - await self._relay_init_update_state(response.relay.value == 1) - return self._relay_config.init_state - return None - async def _relay_init_set(self, state: bool) -> bool | None: + try: + request = CircleRelayInitStateRequest( + self._send, self._mac_in_bytes, False, False + ) + except MessageError as err: + raise NodeError(f"{self._mac_in_str} error: {err}") from err + + if (response := await request.send()) is None: + _LOGGER.warning( + "No response from %s to get relay init setting", self._mac_in_str + ) + return None + + if isinstance(response, CircleRelayInitStateResponse): + _LOGGER.debug("Successful get of relay init state for %s", self._mac_in_str) + state = response.relay.value == 1 + await self._relay_init_update_state(state) + return state + + async def _relay_init_set(self, state: bool) -> None: """Configure relay init state.""" if NodeFeature.RELAY_INIT not in self._features: raise NodeError( "Configuring of initial state of relay is not" + f"supported for device {self.name}" ) - request = CircleRelayInitStateRequest( - self._send, self._mac_in_bytes, True, state - ) - if (response := await request.send()) is not None: - await self._relay_init_update_state(response.relay.value == 1) - return self._relay_config.init_state - return None + + try: + request = CircleRelayInitStateRequest( + self._send, self._mac_in_bytes, True, state + ) + except MessageError as err: + raise NodeError(f"{self._mac_in_str} error: {err}") from err + + if (response := await request.send()) is None: + _LOGGER.warning( + "No response from %s to configure relay init setting", self._mac_in_str + ) + return None + + if response.node_ack_type == NodeAckResponseType.DEFAULT_FAIL: + _LOGGER.warning("Failed to set relay init state for %s", self._mac_in_str) + return None + + if response.node_ack_type == NodeAckResponseType.DEFAULT_ACK: + _LOGGER.debug("Successful set relay init state for %s", self._mac_in_str) + await self._relay_init_update_state(state) async def _relay_init_load_from_cache(self) -> bool: """Load relay init state from cache. Returns True if retrieval was successful.""" diff --git a/pyproject.toml b/pyproject.toml index 55e13abd7..8d6a058d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.47.5" +version = "0.47.6" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [