Handle daily events, weekly recurring events optimize events calculation

This commit is contained in:
nemunaire 2022-09-19 16:06:02 +02:00
parent d3c24e6f3e
commit 4d7d1ee0f4

View File

@ -1,15 +1,23 @@
from datetime import datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from functools import reduce
import hashlib import hashlib
import re import re
import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from icalendar import Calendar, Event, vCalAddress, vText from icalendar import Calendar, Event, vCalAddress, vDDDTypes, vText
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import pytz import pytz
from . import display_longtext from . import display_longtext
def next_weekday(d, weekday):
days_ahead = weekday - d.weekday()
if days_ahead < 0: # Target day already happened this week
days_ahead += 7
return d + timedelta(days_ahead)
class IcalModule: class IcalModule:
def __init__(self, config): def __init__(self, config):
@ -17,11 +25,17 @@ class IcalModule:
self.delayed_departure = 100 self.delayed_departure = 100
self._cached_events = []
self._cached_file = ".ical-%s.cache" self._cached_file = ".ical-%s.cache"
self.cache_time = 15 self.cache_time = 15
def get_events(self, config, toofar=None, now=None): def __get_events(self, config, toofar=None):
today = datetime.now(tz=pytz.timezone('Europe/Paris')).replace(hour=0, minute=0, second=0) today = datetime.now(tz=pytz.timezone('Europe/Paris')).replace(hour=0, minute=0, second=0)
todayd = date.today()
toofard = None
if toofar is not None:
toofard = date(toofar.year, toofar.month, toofar.day)
events = [] events = []
for cal in self.cals: for cal in self.cals:
@ -42,10 +56,61 @@ class IcalModule:
ecal = Calendar.from_ical(c.read()) ecal = Calendar.from_ical(c.read())
for component in ecal.walk(): for component in ecal.walk():
if component.name == "VEVENT": if component.name == "VEVENT":
if component.decoded("DTEND") < today: start = component.decoded("DTSTART")
end = component.decoded("DTEND")
if "RRULE" in component:
rrule = component.decoded("RRULE")
if "UNTIL" in rrule and reduce(lambda p,d: p or d < today, rrule["UNTIL"], False):
continue continue
if toofar is not None and component.decoded("DTSTART") > toofar: if "FREQ" in rrule and "BYDAY" in rrule and "WEEKLY" in rrule["FREQ"]:
weekday = 6
if "MO" in rrule["BYDAY"]:
weekday = 0
elif "TU" in rrule["BYDAY"]:
weekday = 1
elif "WE" in rrule["BYDAY"]:
weekday = 2
elif "TH" in rrule["BYDAY"]:
weekday = 3
elif "FR" in rrule["BYDAY"]:
weekday = 4
elif "SA" in rrule["BYDAY"]:
weekday = 5
start = next_weekday(today, weekday).replace(hour=start.hour, minute=start.minute, second=start.second, microsecond=start.microsecond)
end = next_weekday(today, weekday).replace(hour=end.hour, minute=end.minute, second=end.second, microsecond=end.microsecond)
component["DTSTART"] = vDDDTypes(start)
component["DTEND"] = vDDDTypes(end)
if isinstance(end, datetime):
if end < today:
continue continue
if toofar is not None and start > toofar:
continue
else:
if isinstance(end, date) and end < todayd:
continue
if toofard is not None and start > toofard:
continue
events.append(component)
# Sort events
events.sort(key=lambda e: time.mktime(e.decoded("DTSTART").timetuple()))
return events
def _get_events(self, config, toofar=None):
if len(self._cached_events) == 0:
evts = self.__get_events(config, toofar)
self._cached_events = evts
return self._cached_events
def get_events(self, config, toofar=None, now=None):
for component in self._get_events(config, toofar):
evt = None
train_situations, train_conjoncturels, train_start_station, train_end_station, place = self.is_train_event(component) train_situations, train_conjoncturels, train_start_station, train_end_station, place = self.is_train_event(component)
if train_start_station is not None: if train_start_station is not None:
@ -86,8 +151,7 @@ class IcalModule:
if "voie" in train_end_station and "numero" in train_end_station["voie"]: if "voie" in train_end_station and "numero" in train_end_station["voie"]:
evt["info"] += " voie " + train_end_station["voie"]["numero"] evt["info"] += " voie " + train_end_station["voie"]["numero"]
events.append(evt) elif now is not None and time.mktime(component.decoded("DTEND").timetuple()) < time.mktime(now.timetuple()):
elif now is not None and component.decoded("DTEND") < now:
continue continue
else: else:
evt = { evt = {
@ -99,12 +163,8 @@ class IcalModule:
if "location" in component: if "location" in component:
evt["location"] = component.decoded("LOCATION").decode() evt["location"] = component.decoded("LOCATION").decode()
events.append(evt) if evt is not None:
yield evt
# Sort events
events.sort(key=lambda e: e["start"])
return events
def is_train_event(self, evt): def is_train_event(self, evt):
if "description" not in evt: if "description" not in evt:
@ -165,18 +225,18 @@ class IcalModule:
return situations, conjoncturels, start_station, end_station, place return situations, conjoncturels, start_station, end_station, place
def event_coming(self, config): def event_coming(self, config):
now = datetime.now(tz=pytz.timezone('Europe/Paris')) now = time.mktime(datetime.now(tz=pytz.timezone('Europe/Paris')).timetuple())
coming = datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure) coming = time.mktime((datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure)).timetuple())
for evt in self.get_events(config): for evt in self.get_events(config):
# Looking only the first event # Looking only the first event
start = evt["start"] start = time.mktime(evt["start"].timetuple())
return now < start and start < coming return now < start and start < coming
return False return False
def non_local_event_coming(self, config): def non_local_event_coming(self, config):
now = datetime.now(tz=pytz.timezone('Europe/Paris')) now = time.mktime(datetime.now(tz=pytz.timezone('Europe/Paris')).timetuple())
coming = datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure) coming = time.mktime((datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure)).timetuple())
for evt in self.get_events(config): for evt in self.get_events(config):
if "location" in evt and ( if "location" in evt and (
@ -188,20 +248,20 @@ class IcalModule:
continue continue
# Looking only the first event # Looking only the first event
start = evt["start"] start = time.mktime(evt["start"].timetuple())
return now < start and start < coming return now < start and start < coming
return False return False
def local_event_ending(self, config): def local_event_ending(self, config):
now = datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure/3) now = time.mktime((datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure/3)).timetuple())
coming = datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure/3) coming = time.mktime((datetime.now(tz=pytz.timezone('Europe/Paris')) + timedelta(minutes=self.delayed_departure/3)).timetuple())
for evt in self.get_events(config): for evt in self.get_events(config):
if not("location" in evt and evt["location"].lower().startswith("ici")): if not("location" in evt and evt["location"].lower().startswith("ici")):
continue continue
# Looking only the first event # Looking only the first event
end = evt["end"] end = time.mktime(evt["end"].timetuple())
return now < end and end < coming return now < end and end < coming
return False return False
@ -218,20 +278,33 @@ class IcalModule:
last_evt = None last_evt = None
align = 0 align = 0
for evt in events: for evt in events:
if last_evt is None or last_evt["start"].astimezone(pytz.timezone('Europe/Paris')).day != evt["start"].astimezone(pytz.timezone('Europe/Paris')).day: last_day = None
if last_evt is not None:
last_day = last_evt["start"].astimezone(pytz.timezone('Europe/Paris')).day if isinstance(last_evt["start"], datetime) else last_evt["start"].timetuple()[2]
cur_day = evt["start"].astimezone(pytz.timezone('Europe/Paris')).day if isinstance(evt["start"], datetime) else evt["start"].timetuple()[2]
if last_day is None or last_day != cur_day:
draw.text( draw.text(
(width / 2, align), (width / 2, align),
evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%a %d %B"), evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%a %d %B") if isinstance(evt["start"], datetime) else evt["start"].strftime("%a %d %B"),
fill="black", anchor="mt", font=fnt_B fill="black", anchor="mt", font=fnt_B
) )
align += line_height align += line_height
draw.rectangle((0, align-4, width, align-4), fill="black") draw.rectangle((0, align-4, width, align-4), fill="black")
if isinstance(evt["start"], datetime):
draw.text( draw.text(
(2, align), (2, align),
evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M") + " " + evt["summary"], evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M") + " " + evt["summary"],
fill="black", anchor="lt", font=fnt_R fill="black", anchor="lt", font=fnt_R
) )
else:
draw.text(
(width/2, align),
evt["summary"],
fill="black", anchor="mt", font=fnt_B
)
if "new_start" in evt: if "new_start" in evt:
draw.line( draw.line(
(2, align + line_height / 2.6, 2 + fnt_R.getsize(evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M"))[0], align + line_height / 2.6), (2, align + line_height / 2.6, 2 + fnt_R.getsize(evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M"))[0], align + line_height / 2.6),
@ -261,6 +334,8 @@ class IcalModule:
evt["info"], evt["info"],
fill="black", anchor="lt", font=fnt_R, maxwidth=width - fnt_R.getsize(evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M "))[0] fill="black", anchor="lt", font=fnt_R, maxwidth=width - fnt_R.getsize(evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M "))[0]
) )
elif "end" in evt and not isinstance(evt["end"], datetime):
pass
elif "end" in evt and (("new_start" in evt and now > evt["new_start"]) or ("new_start" not in evt and now > evt["start"])): elif "end" in evt and (("new_start" in evt and now > evt["new_start"]) or ("new_start" not in evt and now > evt["start"])):
align += display_longtext(draw, align += display_longtext(draw,
(2 + fnt_R.getsize(evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M "))[0], align+line_height*0.6), (2 + fnt_R.getsize(evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M "))[0], align+line_height*0.6),