22
33from __future__ import annotations
44
5- from asyncio import Task , create_task , gather
5+ from asyncio import CancelledError , Task , create_task , gather , sleep
66from collections .abc import Awaitable , Callable
77from dataclasses import replace
88from datetime import UTC , datetime , timedelta
99from functools import wraps
1010import logging
1111from math import ceil
12+ import random
1213from typing import Any , Final , TypeVar , cast
1314
1415from ..api import (
7475# Default firmware if not known
7576DEFAULT_FIRMWARE : Final = datetime (2008 , 8 , 26 , 15 , 46 , tzinfo = UTC )
7677
77- MAX_LOG_HOURS = DAY_IN_HOURS
78+ MAX_LOG_HOURS : Final = DAY_IN_HOURS
79+
80+ CLOCK_SYNC_PERIOD : Final = 3600
7881
7982FuncT = TypeVar ("FuncT" , bound = Callable [..., Any ])
8083_LOGGER = logging .getLogger (__name__ )
@@ -141,6 +144,8 @@ def __init__(
141144 """Initialize base class for Sleeping End Device."""
142145 super ().__init__ (mac , node_type , controller , loaded_callback )
143146
147+ # Clock
148+ self ._clock_synchronize_task : Task [None ] | None = None
144149 # Relay
145150 self ._relay_lock : RelayLock = RelayLock ()
146151 self ._relay_state : RelayState = RelayState ()
@@ -852,47 +857,61 @@ async def _relay_update_lock(
852857 )
853858 await self .save_cache ()
854859
860+ async def _clock_synchronize_scheduler (self ) -> None :
861+ """Background task: periodically synchronize the clock until cancelled."""
862+ try :
863+ while True :
864+ await sleep (CLOCK_SYNC_PERIOD + (random .uniform (- 5 , 5 )))
865+ try :
866+ await self .clock_synchronize ()
867+ except Exception :
868+ _LOGGER .exception (
869+ "Clock synchronization failed for %s" , self ._mac_in_str
870+ )
871+ except CancelledError :
872+ _LOGGER .debug ("Clock sync scheduler cancelled for %s" , self ._mac_in_str )
873+ raise
874+
855875 async def clock_synchronize (self ) -> bool :
856876 """Synchronize clock. Returns true if successful."""
857- get_clock_request = CircleClockGetRequest (self ._send , self ._mac_in_bytes )
858- clock_response = await get_clock_request .send ()
859- if clock_response is None or clock_response .timestamp is None :
877+ request = CircleClockGetRequest (self ._send , self ._mac_in_bytes )
878+ response = await request .send ()
879+ if response is None or response .timestamp is None :
860880 return False
861- _dt_of_circle = datetime .now (tz = UTC ).replace (
862- hour = clock_response .time .hour .value ,
863- minute = clock_response .time .minute .value ,
864- second = clock_response .time .second .value ,
881+
882+ dt_now = datetime .now (tz = UTC )
883+ days_diff = (response .day_of_week .value - dt_now .weekday ()) % 7
884+ circle_timestamp : datetime = dt_now .replace (
885+ day = dt_now .day + days_diff ,
886+ hour = response .time .value .hour ,
887+ minute = response .time .value .minute ,
888+ second = response .time .value .second ,
865889 microsecond = 0 ,
866890 tzinfo = UTC ,
867891 )
868- clock_offset = clock_response .timestamp .replace (microsecond = 0 ) - _dt_of_circle
869- if (clock_offset .seconds < MAX_TIME_DRIFT ) or (
870- clock_offset .seconds > - (MAX_TIME_DRIFT )
871- ):
892+ clock_offset = response .timestamp .replace (microsecond = 0 ) - circle_timestamp
893+ if abs (clock_offset .total_seconds ()) < MAX_TIME_DRIFT :
872894 return True
895+
873896 _LOGGER .info (
874- "Reset clock of node %s because time has drifted %s sec " ,
897+ "Sync clock of node %s because time drifted %s seconds " ,
875898 self ._mac_in_str ,
876- str ( clock_offset .seconds ),
899+ int ( abs ( clock_offset .total_seconds ()) ),
877900 )
878901 if self ._node_protocols is None :
879902 raise NodeError (
880- "Unable to synchronize clock en when protocol version is unknown"
903+ "Unable to synchronize clock when protocol version is unknown"
881904 )
882- set_clock_request = CircleClockSetRequest (
905+
906+ set_request = CircleClockSetRequest (
883907 self ._send ,
884908 self ._mac_in_bytes ,
885909 datetime .now (tz = UTC ),
886910 self ._node_protocols .max ,
887911 )
888- if (node_response := await set_clock_request .send ()) is None :
889- _LOGGER .warning (
890- "Failed to (re)set the internal clock of %s" ,
891- self .name ,
892- )
893- return False
894- if node_response .ack_id == NodeResponseType .CLOCK_ACCEPTED :
895- return True
912+ if (node_response := await set_request .send ()) is not None :
913+ return node_response .ack_id == NodeResponseType .CLOCK_ACCEPTED
914+ _LOGGER .warning ("Failed to sync the clock of %s" , self .name )
896915 return False
897916
898917 async def load (self ) -> None :
@@ -1016,6 +1035,10 @@ async def initialize(self) -> bool:
10161035 return False
10171036
10181037 await super ().initialize ()
1038+ if self ._clock_synchronize_task is None or self ._clock_synchronize_task .done ():
1039+ self ._clock_synchronize_task = create_task (
1040+ self ._clock_synchronize_scheduler ()
1041+ )
10191042 return True
10201043
10211044 async def node_info_update (
@@ -1082,6 +1105,11 @@ async def unload(self) -> None:
10821105 if self ._cache_enabled :
10831106 await self ._energy_log_records_save_to_cache ()
10841107
1108+ if self ._clock_synchronize_task :
1109+ self ._clock_synchronize_task .cancel ()
1110+ await gather (self ._clock_synchronize_task , return_exceptions = True )
1111+ self ._clock_synchronize_task = None
1112+
10851113 await super ().unload ()
10861114
10871115 @raise_not_loaded
@@ -1318,7 +1346,7 @@ async def energy_reset_request(self) -> None:
13181346 f"Unexpected NodeResponseType { response .ack_id !r} received as response to CircleClockSetRequest"
13191347 )
13201348
1321- _LOGGER .warning ("Energy reset for Node %s successful" , self ._mac_in_str )
1349+ _LOGGER .info ("Energy reset for Node %s successful" , self ._mac_in_str )
13221350
13231351 # Follow up by an energy-intervals (re)set
13241352 interval_request = CircleMeasureIntervalRequest (
@@ -1337,20 +1365,20 @@ async def energy_reset_request(self) -> None:
13371365 raise MessageError (
13381366 f"Unknown NodeResponseType '{ interval_response .response_type .name } ' received"
13391367 )
1340- _LOGGER .warning ("Resetting energy intervals to default (= consumption only)" )
1368+ _LOGGER .info ("Resetting energy intervals to default (= consumption only)" )
13411369
13421370 # Clear the cached energy_collection
13431371 if self ._cache_enabled :
13441372 self ._set_cache (CACHE_ENERGY_COLLECTION , "" )
1345- _LOGGER .warning (
1373+ _LOGGER .info (
13461374 "Energy-collection cache cleared successfully, updating cache for %s" ,
13471375 self ._mac_in_str ,
13481376 )
13491377 await self .save_cache ()
13501378
13511379 # Clear PulseCollection._logs
13521380 self ._energy_counters .reset_pulse_collection ()
1353- _LOGGER .warning ("Resetting pulse-collection" )
1381+ _LOGGER .info ("Resetting pulse-collection" )
13541382
13551383 # Request a NodeInfo update
13561384 if await self .node_info_update () is None :
@@ -1359,7 +1387,7 @@ async def energy_reset_request(self) -> None:
13591387 self ._mac_in_str ,
13601388 )
13611389 else :
1362- _LOGGER .warning (
1390+ _LOGGER .info (
13631391 "Node info update after energy-reset successful for %s" ,
13641392 self ._mac_in_str ,
13651393 )
0 commit comments