Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Ongoing

- PR [341](https://github.com/plugwise/python-plugwise-usb/pull/341): Schedule clock synchronization every 3600 seconds
- PR [342](https://github.com/plugwise/python-plugwise-usb/pull/342): Improve node_type chaching.

## 0.46.0 - 2025-09-12
Expand Down
8 changes: 6 additions & 2 deletions plugwise_usb/network/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ async def save_cache(self) -> None:
mac: node_type.name for mac, node_type in self._nodetypes.items()
}
_LOGGER.debug("Save NodeTypes for %s Nodes", len(cache_data_to_save))
await self.write_cache(cache_data_to_save, rewrite=True) # Make sure the cache-contents is actual
await self.write_cache(
cache_data_to_save, rewrite=True
) # Make sure the cache-contents is actual

async def clear_cache(self) -> None:
"""Clear current cache."""
Expand All @@ -54,7 +56,9 @@ async def restore_cache(self) -> None:
node_type = None

if node_type is None:
_LOGGER.warning("Invalid NodeType in cache for mac %s: %s", mac, node_value)
_LOGGER.warning(
"Invalid NodeType in cache for mac %s: %s", mac, node_value
)
continue
self._nodetypes[mac] = node_type
_LOGGER.debug(
Expand Down
41 changes: 34 additions & 7 deletions plugwise_usb/nodes/circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from asyncio import Task, create_task, gather
from asyncio import CancelledError, Task, create_task, gather, sleep
from collections.abc import Awaitable, Callable
from dataclasses import replace
from datetime import UTC, datetime, timedelta
Expand Down Expand Up @@ -74,7 +74,9 @@
# Default firmware if not known
DEFAULT_FIRMWARE: Final = datetime(2008, 8, 26, 15, 46, tzinfo=UTC)

MAX_LOG_HOURS = DAY_IN_HOURS
MAX_LOG_HOURS: Final = DAY_IN_HOURS

CLOCK_SYNC_PERIOD: Final = 3600

FuncT = TypeVar("FuncT", bound=Callable[..., Any])
_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -141,6 +143,8 @@ def __init__(
"""Initialize base class for Sleeping End Device."""
super().__init__(mac, node_type, controller, loaded_callback)

# Clock
self._clock_synchronize_task: Task[None] | None = None
# Relay
self._relay_lock: RelayLock = RelayLock()
self._relay_state: RelayState = RelayState()
Expand Down Expand Up @@ -852,6 +856,21 @@ async def _relay_update_lock(
)
await self.save_cache()

async def _clock_synchronize_scheduler(self) -> None:
"""Background task: periodically synchronize the clock until cancelled."""
try:
while True:
await sleep(CLOCK_SYNC_PERIOD + (random.uniform(-5, 5)))
try:
await self.clock_synchronize()
except Exception:
_LOGGER.exception(
"Clock synchronization failed for %s", self._mac_in_str
)
except CancelledError:
_LOGGER.debug("Clock sync scheduler cancelled for %s", self._mac_in_str)
raise

Comment thread
dirixmjm marked this conversation as resolved.
async def clock_synchronize(self) -> bool:
"""Synchronize clock. Returns true if successful."""
get_clock_request = CircleClockGetRequest(self._send, self._mac_in_bytes)
Expand All @@ -866,14 +885,13 @@ async def clock_synchronize(self) -> bool:
tzinfo=UTC,
)
clock_offset = clock_response.timestamp.replace(microsecond=0) - _dt_of_circle
if (clock_offset.seconds < MAX_TIME_DRIFT) or (
clock_offset.seconds > -(MAX_TIME_DRIFT)
):
if abs(clock_offset.total_seconds()) < MAX_TIME_DRIFT:
return True
_LOGGER.info(
"Reset clock of node %s because time has drifted %s sec",
"Reset clock of node %s because time drifted %s seconds (max %s seconds)",
self._mac_in_str,
str(clock_offset.seconds),
str(int(abs(clock_offset.total_seconds()))),
str(MAX_TIME_DRIFT),
)
if self._node_protocols is None:
raise NodeError(
Expand Down Expand Up @@ -992,6 +1010,10 @@ async def initialize(self) -> bool:
)
self._initialized = False
return False
if self._clock_synchronize_task is None or self._clock_synchronize_task.done():
self._clock_synchronize_task = create_task(
self._clock_synchronize_scheduler()
)

if not self._calibration and not await self.calibration_update():
_LOGGER.debug(
Expand Down Expand Up @@ -1082,6 +1104,11 @@ async def unload(self) -> None:
if self._cache_enabled:
await self._energy_log_records_save_to_cache()

if self._clock_synchronize_task:
self._clock_synchronize_task.cancel()
await gather(self._clock_synchronize_task, return_exceptions=True)
self._clock_synchronize_task = None

await super().unload()

@raise_not_loaded
Expand Down
6 changes: 2 additions & 4 deletions plugwise_usb/nodes/circle_plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,12 @@ async def clock_synchronize(self) -> bool:
tzinfo=UTC,
)
clock_offset = clock_response.timestamp.replace(microsecond=0) - _dt_of_circle
if (clock_offset.seconds < MAX_TIME_DRIFT) or (
clock_offset.seconds > -(MAX_TIME_DRIFT)
):
if abs(clock_offset.total_seconds()) < MAX_TIME_DRIFT:
return True
_LOGGER.info(
"Reset realtime clock of node %s because time has drifted %s seconds while max drift is set to %s seconds)",
self._node_info.mac,
str(clock_offset.seconds),
str(int(abs(clock_offset.total_seconds()))),
str(MAX_TIME_DRIFT),
)
clock_set_request = CirclePlusRealTimeClockSetRequest(
Expand Down
Loading