fix(alarmclock): fiabiliser le rafraîchissement du calendrier ICS

- Ajouter deux tentatives de reprise après 2 puis 5 minutes en cas d’échec.
- Mémoriser le dernier succès et adapter le niveau des logs à l’ancienneté du calendrier.
- Distinguer les erreurs réseau, HTTP et les données ICS invalides.
- Définir les délais de connexion et de lecture et vérifier le statut HTTP.
- Actualiser le statut du jour travaillé et l’heure du réveil après une reprise.
This commit is contained in:
Pierre committed 2026-11-10 19:31:13 +01:00
1 parent ff39f4cb2f
commit 9802982d73
1 file changed
+81 -8
+81 -8
View File
@@ -32,7 +32,19 @@ class CalendarEvent:
class AlarmClock(SmartObject): class AlarmClock(SmartObject):
CALENDAR_RETRY_DELAYS = (2 * 60, 5 * 60)
@property
def calendar_last_success(self):
return self.dataset.get('calendar_last_success')
@calendar_last_success.setter
def calendar_last_success(self, value):
self.dataset['calendar_last_success'] = value
def on_initialize_smart_object(self): def on_initialize_smart_object(self):
if self.dataset is None:
self.dataset = {}
self.switch_alarmclock_main = self.args['switch_alarmclock_main'] self.switch_alarmclock_main = self.args['switch_alarmclock_main']
self.switch_alarmclock_secondary = self.args['switch_alarmclock_secondary'] self.switch_alarmclock_secondary = self.args['switch_alarmclock_secondary']
self.inputdate_alarmclock_main = self.args['inputdate_alarmclock_main'] self.inputdate_alarmclock_main = self.args['inputdate_alarmclock_main']
@@ -46,6 +58,8 @@ class AlarmClock(SmartObject):
self.handle_program_next_wakueup_cb = None self.handle_program_next_wakueup_cb = None
self.handle_prewakueup_cb = None self.handle_prewakueup_cb = None
self.handle_wakueup_cb = None self.handle_wakueup_cb = None
self.calendar_retry_handle = None
self.calendar_retry_attempt = 0
self.handle_switch_reveil_principal = self.listen_state(self.on_alarm_clock_set, self.switch_alarmclock_main) self.handle_switch_reveil_principal = self.listen_state(self.on_alarm_clock_set, self.switch_alarmclock_main)
self.handle_switch_reveil_secondaire = self.listen_state(self.on_alarm_clock_set, self.switch_alarmclock_secondary) self.handle_switch_reveil_secondaire = self.listen_state(self.on_alarm_clock_set, self.switch_alarmclock_secondary)
self.listen_state(self.on_alarm_clock_set, self.inputdate_alarmclock_main) self.listen_state(self.on_alarm_clock_set, self.inputdate_alarmclock_main)
@@ -64,17 +78,64 @@ class AlarmClock(SmartObject):
def refresh_cal(self): def refresh_cal(self):
with self.log_group("refresh_cal"): with self.log_group("refresh_cal"):
new_cal = list() new_cal = list()
#if self.load_cal_from_google_calendar(new_cal): #if self.load_cal_from_google_calendar(new_cal):
if self.load_cal_from_ics(new_cal): if self.load_cal_from_ics(new_cal):
self.calendar_last_success = datetime.datetime.now(datetime.timezone.utc)
self.calendar_retry_attempt = 0
self._cancel_calendar_retry()
if not self.are_cal_identical(self.calendar,new_cal): if not self.are_cal_identical(self.calendar,new_cal):
self.calendar = new_cal self.calendar = new_cal
self.log_info("Calendar has been updated!") self.log_info("Calendar has been updated!")
self.print_cal(self.calendar) self.print_cal(self.calendar)
return True return True
else:
self._schedule_calendar_retry()
return False return False
def _log_calendar_failure(self, reason):
last_success = self.calendar_last_success
if last_success is None:
log = self.log_error
freshness = 'no successful refresh recorded'
else:
age = datetime.datetime.now(datetime.timezone.utc) - last_success
if age < datetime.timedelta(hours=24):
log = self.log_info
elif age < datetime.timedelta(hours=48):
log = self.log_warning
else:
log = self.log_error
freshness = (f'last_success={last_success.isoformat()}, '
f'age={age.total_seconds() / 3600:.2f}h')
log(f"Couldn't load calendar: {reason}; {freshness}")
def _schedule_calendar_retry(self):
if self.calendar_retry_handle is not None:
return
if self.calendar_retry_attempt >= len(self.CALENDAR_RETRY_DELAYS):
return
delay = self.CALENDAR_RETRY_DELAYS[self.calendar_retry_attempt]
self.calendar_retry_attempt += 1
self.calendar_retry_handle = self.run_in(self.on_calendar_retry, delay)
self.log_info(f"Calendar refresh retry scheduled in {delay // 60} minutes")
def _cancel_calendar_retry(self):
if self.calendar_retry_handle is None:
return
if self.timer_running(self.calendar_retry_handle):
self.cancel_timer(self.calendar_retry_handle)
self.calendar_retry_handle = None
def on_calendar_retry(self, kwargs):
self.calendar_retry_handle = None
calendar_changed = self.refresh_cal()
self.update_workday_status()
if calendar_changed:
self.update_wakeup_time()
def are_cal_identical(self,calendar1,calendar2): def are_cal_identical(self,calendar1,calendar2):
if len(calendar1) != len(calendar2): return False if len(calendar1) != len(calendar2): return False
@@ -93,7 +154,8 @@ class AlarmClock(SmartObject):
try: try:
self.log_info(f"Retrieving calengar from outlook.office365.com") self.log_info(f"Retrieving calengar from outlook.office365.com")
r = get("https://outlook.office365.com/owa/calendar/0c64359c70eb41cd9713fe92c470d3df@ubisoft.com/5edb3fb5150f4203b66d9cf8db3184be11626033572842691749/calendar.ics", timeout=10)#, headers=headers, verify=False) r = get("https://outlook.office365.com/owa/calendar/0c64359c70eb41cd9713fe92c470d3df@ubisoft.com/5edb3fb5150f4203b66d9cf8db3184be11626033572842691749/calendar.ics", timeout=(3, 5))#, headers=headers, verify=False)
r.raise_for_status()
#self.log_info(f"Processing Calendar") #self.log_info(f"Processing Calendar")
today = datetime.datetime.combine(datetime.date.today(), datetime.time(0,0, 0)) today = datetime.datetime.combine(datetime.date.today(), datetime.time(0,0, 0))
@@ -101,7 +163,7 @@ class AlarmClock(SmartObject):
end_load_date = today + datetime.timedelta(days=2) end_load_date = today + datetime.timedelta(days=2)
#self.log_info("[" + str(start_load_date) + "]-[" + str(end_load_date) + "]") #self.log_info("[" + str(start_load_date) + "]-[" + str(end_load_date) + "]")
calendar_ubi = icalendar.Calendar.from_ical(r.text) calendar_ubi = icalendar.Calendar.from_ical(r.content)
for component in calendar_ubi.walk(): for component in calendar_ubi.walk():
if component.name == "VEVENT": if component.name == "VEVENT":
event_summary = component.get('summary') event_summary = component.get('summary')
@@ -137,11 +199,20 @@ class AlarmClock(SmartObject):
calendar.append(CalendarEvent(start_datetime,end_datetime,event_type)) calendar.append(CalendarEvent(start_datetime,end_datetime,event_type))
else: self.log_warning(f"[Unknow event : {event_summary}]Start at {start_datetime},Finish at {end_datetime}") else: self.log_warning(f"[Unknow event : {event_summary}]Start at {start_datetime},Finish at {end_datetime}")
except ValueError: except requests.exceptions.Timeout as error:
self.log_error("Couldn't load calendar") self._log_calendar_failure(f"request timed out ({type(error).__name__})")
return False return False
except (requests.exceptions.ConnectionError,requests.exceptions.ReadTimeout): except requests.exceptions.ConnectionError:
self.log_warning("Couldn't load calendar, request Timeout") self._log_calendar_failure("connection failed")
return False
except requests.exceptions.HTTPError as error:
self._log_calendar_failure(f"HTTP {error.response.status_code}")
return False
except requests.exceptions.RequestException as error:
self._log_calendar_failure(type(error).__name__)
return False
except (ValueError, TypeError, KeyError):
self._log_calendar_failure("invalid ICS data")
return False return False
#self.log_info(f"Calendar loaded") #self.log_info(f"Calendar loaded")
@@ -151,6 +222,8 @@ class AlarmClock(SmartObject):
self.update_wakeup_time() self.update_wakeup_time()
def on_refresh_cal(self, kwargs): def on_refresh_cal(self, kwargs):
self._cancel_calendar_retry()
self.calendar_retry_attempt = 0
calendar_changed = self.refresh_cal() calendar_changed = self.refresh_cal()
self.update_workday_status() self.update_workday_status()
if calendar_changed: if calendar_changed: