diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5f7d1ab..2cae2b68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.42.0 + +- Implement resetting of energy logs + ## v0.41.0 - Implement setting of energy logging intervals [#247](https://github.com/plugwise/python-plugwise-usb/pull/247) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 06222ab89..ebbf8e587 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -10,10 +10,11 @@ from collections.abc import Callable, Coroutine from functools import wraps import logging -from typing import Any, TypeVar, cast, Final +from typing import Any, Final, TypeVar, cast from .api import NodeEvent, PlugwiseNode, StickEvent from .connection import StickController +from .constants import DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL from .exceptions import MessageError, NodeError, StickError, SubscriptionError from .network import StickNetwork @@ -210,6 +211,24 @@ async def set_accept_join_request(self, state: bool) -> bool: 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) + try: + await self._network.energy_reset_request(mac) + except (MessageError, NodeError) as exc: + raise NodeError(f"{exc}") from exc + + # Follow up by an energy-intervals (re)set + if ( + result := await self.set_energy_intervals( + mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL + ) + ): + return result + + return False + async def set_energy_intervals( self, mac: str, cons_interval: int, prod_interval: int ) -> bool: @@ -295,7 +314,7 @@ async def connect(self, port: str | None = None) -> None: "Unable to connect. " + "Path to USB-Stick is not defined, set port property first" ) - + await self._controller.connect_to_stick( self._port, ) diff --git a/plugwise_usb/constants.py b/plugwise_usb/constants.py index 2e4f4441c..42539e97f 100644 --- a/plugwise_usb/constants.py +++ b/plugwise_usb/constants.py @@ -31,7 +31,7 @@ # Max timeout in seconds # Stick responds with timeout messages within 10s. -STICK_TIME_OUT: Final = 11 +STICK_TIME_OUT: Final = 11 # In bigger networks a response from a Node could take up a while, so lets use 15 seconds. NODE_TIME_OUT: Final = 15 @@ -99,3 +99,10 @@ 8: ("Celsius",), 9: ("Stealth",), } + +# Energy logging intervals +DEFAULT_CONS_INTERVAL: Final[int] = 60 +NO_PRODUCTION_INTERVAL: Final[int] = 0 + +# Energy Node types +ENERGY_NODE_TYPES: tuple[int] = (1, 2, 9) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 3f9e49c26..d01bd6b08 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -12,7 +12,6 @@ from ..constants import ( DAY_IN_MINUTES, HOUR_IN_MINUTES, - LOGADDR_OFFSET, MAX_RETRIES, MESSAGE_FOOTER, MESSAGE_HEADER, @@ -765,7 +764,7 @@ def __init__( if reset: self._args += [ this_date, - LogAddr(LOGADDR_OFFSET, 8, False), + LogAddr(0, 8, False), this_time, day_of_week, ] diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index cce2cea65..948089326 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -6,17 +6,19 @@ from asyncio import Task, create_task, gather, sleep from collections.abc import Callable, Coroutine -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import logging from typing import Any from ..api import NodeEvent, NodeType, PlugwiseNode, StickEvent from ..connection import StickController -from ..constants import UTF8 +from ..constants import ENERGY_NODE_TYPES, UTF8 from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout +from ..helpers.util import validate_mac from ..messages.requests import ( - CirclePlusAllowJoiningRequest, + CircleClockSetRequest, CircleMeasureIntervalRequest, + CirclePlusAllowJoiningRequest, NodePingRequest, ) from ..messages.responses import ( @@ -541,6 +543,25 @@ async def allow_join_requests(self, state: bool) -> None: _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) + node_protocols = self._nodes[mac].node_protocols + request = CircleClockSetRequest( + self._controller.send, + bytes(mac, UTF8), + datetime.now(tz=UTC), + node_protocols.max, + True, + ) + if (response := await request.send()) is None: + raise NodeError(f"Energy-reset for {mac} failed") + + if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: + raise MessageError( + f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" + ) + async def set_energy_intervals( self, mac: str, consumption: int, production: int ) -> None: @@ -549,7 +570,7 @@ async def set_energy_intervals( Default: consumption = 60, production = 0. For logging energy in both directions set both to 60. """ - # Validate input parameters + self._validate_energy_node(mac) if consumption <= 0: raise ValueError("Consumption interval must be positive") if production < 0: @@ -569,6 +590,19 @@ async def set_energy_intervals( f"Unknown NodeResponseType '{response.response_type.name}' received" ) + def _validate_energy_node(self, mac: str) -> None: + """Validate node for energy operations.""" + if not validate_mac(mac): + raise NodeError(f"MAC '{mac}' invalid") + + if mac not in self._nodes: + raise NodeError(f"Node {mac} not present in network") + + if self._nodes[mac].node_info.node_type.value not in ENERGY_NODE_TYPES: + raise NodeError( + f"Energy operations not supported for {self._nodes[mac].node_info.node_type.name}" + ) + def subscribe_to_node_events( self, node_event_callback: Callable[[NodeEvent, str], Coroutine[Any, Any, None]], diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index d73d5933e..d5d78d402 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -100,6 +100,14 @@ def available_state(self) -> AvailableState: self._last_seen, ) + @property + def node_protocols(self) -> SupportedVersions | None: + """Return the node_protocols for the Node.""" + if self._node_protocols is None: + return None + + return self._node_protocols + @property @raise_not_loaded def battery_config(self) -> BatteryConfig: diff --git a/pyproject.toml b/pyproject.toml index 374f9c5cc..2e3af094f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.41.0" +version = "0.42.0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [