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 numberDiff line numberDiff line change
@@ -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)
Expand Down
23 changes: 21 additions & 2 deletions plugwise_usb/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand DownExpand Up@@ -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:
Expand DownExpand Up@@ -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,
)
Expand Down
9 changes: 8 additions & 1 deletion plugwise_usb/constants.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand DownExpand Up@@ -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)
3 changes: 1 addition & 2 deletions plugwise_usb/messages/requests.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,6 @@
from ..constants import (
DAY_IN_MINUTES,
HOUR_IN_MINUTES,
LOGADDR_OFFSET,
MAX_RETRIES,
MESSAGE_FOOTER,
MESSAGE_HEADER,
Expand DownExpand Up@@ -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,
]
Expand Down
42 changes: 38 additions & 4 deletions plugwise_usb/network/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 (
Expand DownExpand Up@@ -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:
Expand All@@ -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:
Expand All@@ -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]],
Expand Down
8 changes: 8 additions & 0 deletions plugwise_usb/nodes/node.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = [
Expand Down