From 2a31062d13408b81dc3c930609d418f6f8fe48bb Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 3 Dec 2024 21:25:54 +0000 Subject: [PATCH] fix:events_in_the_past don't allow event scheduler to schedule events until clock has been synced since first boot some systems will think we are still in the last century at boot time! the system.clock.synced event needs to be emitted externally by the OS itself, eg https://github.com/TigreGotico/raspOVOS/commit/3ab5f44a189832bf4b61acfcfadabea352092f9e#diff-390ab68cb47cd597ab61ab48741f9d35ce7a7d514a4a40014df1222d402bead8R8 --- ovos_bus_client/util/scheduler.py | 68 +++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/ovos_bus_client/util/scheduler.py b/ovos_bus_client/util/scheduler.py index e615a6c..b9b6f45 100644 --- a/ovos_bus_client/util/scheduler.py +++ b/ovos_bus_client/util/scheduler.py @@ -18,20 +18,20 @@ A scheduled message will be kept and not sent until the system clock time criteria is met. """ - +import datetime import json +import os import shutil import time - -from typing import Optional -from threading import Event from os.path import isfile, join, expanduser +from threading import Event from threading import Thread, Lock +from typing import Optional from ovos_config.config import Configuration -from ovos_config.locations import get_xdg_data_save_path, get_xdg_config_save_path +from ovos_config.locations import get_xdg_data_save_path, get_xdg_config_save_path, get_xdg_cache_save_path from ovos_utils.log import LOG -from ovos_utils.events import EventContainer, EventSchedulerInterface + from ovos_bus_client.message import Message @@ -65,12 +65,25 @@ class EventScheduler(Thread): autostart: if True, start scheduler on init """ - def __init__(self, bus, - schedule_file: str = 'schedule.json', autostart: bool = True): + def __init__(self, bus, schedule_file: str = 'schedule.json', autostart: bool = True): super().__init__() self.events = {} self.event_lock = Lock() + clock_cache = get_xdg_cache_save_path("ovos_clock") + os.makedirs(clock_cache, exist_ok=True) + self.clock_cache = os.path.join(clock_cache, "ovos_clock_sync.ts") + self.last_sync = time.time() + if os.path.isfile(self.clock_cache): + with open(self.clock_cache) as f: + self.last_sync = float(f.read()) + else: + with open(self.clock_cache, "w") as f: + f.write(str(self.last_sync)) + self._dropped_events = 0 + # Convert Unix timestamp to human-readable datetime + pretty_last_sync = datetime.datetime.fromtimestamp(self.last_sync).strftime("%Y-%m-%d %H:%M:%S") + LOG.debug(f"Last clock sync: {pretty_last_sync}") self.bus = bus @@ -85,14 +98,11 @@ def __init__(self, bus, if self.schedule_file: self.load() - self.bus.on('mycroft.scheduler.schedule_event', - self.schedule_event_handler) - self.bus.on('mycroft.scheduler.remove_event', - self.remove_event_handler) - self.bus.on('mycroft.scheduler.update_event', - self.update_event_handler) - self.bus.on('mycroft.scheduler.get_event', - self.get_event_handler) + self.bus.on('mycroft.scheduler.schedule_event', self.schedule_event_handler) + self.bus.on('mycroft.scheduler.remove_event', self.remove_event_handler) + self.bus.on('mycroft.scheduler.update_event', self.update_event_handler) + self.bus.on('mycroft.scheduler.get_event', self.get_event_handler) + self.bus.on('system.clock.synced', self.handle_system_clock_sync) # emitted by raspOVOS self._running = Event() self._stopping = Event() @@ -201,6 +211,17 @@ def schedule_event(self, event: str, sched_time: float, context to send when the handler is called """ + if datetime.datetime.fromtimestamp(self.last_sync) < datetime.datetime(day=1, month=12, year=2024): + # this works around problems in raspOVOS images and other + # systems without RTC that didnt sync clock with the internet yet + # eg. issue demonstration without this: + # date time skill schedulling the hour change sound N times (+1hour every time until present) + LOG.error("Refusing to schedule event, system clock is in the past!") + self._dropped_events += 1 + return + elif datetime.datetime.fromtimestamp(sched_time) < datetime.datetime.now(): + LOG.error("Refusing to schedule event, it is in the past!") + return data = data or {} with self.event_lock: # get current list of scheduled times for event, [] if missing @@ -238,7 +259,7 @@ def schedule_event_handler(self, message: Message): if event and sched_time: self.schedule_event(event, sched_time, repeat, data, context) elif not event: - LOG.error('Scheduled event name not provided') + LOG.error('Scheduled event msg_type not provided') else: LOG.error('Scheduled event time not provided') @@ -253,6 +274,18 @@ def remove_event(self, event: str): if event in self.events: self.events.pop(event) + def handle_system_clock_sync(self, message: Message): + # clock sync, are we in the past? + if self.last_sync - time.time() > datetime.timedelta(hours=6).total_seconds(): + LOG.warning(f"Clock was in the past!!! {self._dropped_events} scheduled events have been dropped") + + self.last_sync = time.time() + # Convert Unix timestamp to human-readable datetime + pretty_last_sync = datetime.datetime.fromtimestamp(self.last_sync).strftime("%Y-%m-%d %H:%M:%S") + LOG.info(f"clock sync: {pretty_last_sync}") + with open(self.clock_cache, "w") as f: + f.write(str(self.last_sync)) + def remove_event_handler(self, message: Message): """ Messagebus interface to the remove_event method. @@ -347,4 +380,3 @@ def shutdown(self): if not isinstance(e, OSError): self.store() raise e -