Handle daily events, weekly recurring events optimize events calculation
This commit is contained in:
parent
d3c24e6f3e
commit
4d7d1ee0f4
221
modules/ical.py
221
modules/ical.py
@ -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,70 +56,116 @@ 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")
|
||||||
continue
|
end = component.decoded("DTEND")
|
||||||
if toofar is not None and component.decoded("DTSTART") > toofar:
|
if "RRULE" in component:
|
||||||
continue
|
rrule = component.decoded("RRULE")
|
||||||
|
if "UNTIL" in rrule and reduce(lambda p,d: p or d < today, rrule["UNTIL"], False):
|
||||||
|
continue
|
||||||
|
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
|
||||||
|
|
||||||
train_situations, train_conjoncturels, train_start_station, train_end_station, place = self.is_train_event(component)
|
start = next_weekday(today, weekday).replace(hour=start.hour, minute=start.minute, second=start.second, microsecond=start.microsecond)
|
||||||
if train_start_station is not None:
|
end = next_weekday(today, weekday).replace(hour=end.hour, minute=end.minute, second=end.second, microsecond=end.microsecond)
|
||||||
start = component.decoded("DTSTART")
|
|
||||||
end = component.decoded("DTEND")
|
|
||||||
evt = {
|
|
||||||
"summary": component.get("SUMMARY"),
|
|
||||||
"start": start,
|
|
||||||
"end": end,
|
|
||||||
}
|
|
||||||
|
|
||||||
new_start = datetime.fromisoformat(train_start_station["depart"]["dateHeureReelle"])
|
component["DTSTART"] = vDDDTypes(start)
|
||||||
if start.astimezone(pytz.timezone('Europe/Paris')).strftime("%d %H:%M") != new_start.strftime("%d %H:%M"):
|
component["DTEND"] = vDDDTypes(end)
|
||||||
evt["new_start"] = new_start
|
|
||||||
|
|
||||||
if train_end_station:
|
if isinstance(end, datetime):
|
||||||
new_end = datetime.fromisoformat(train_end_station["arrivee"]["dateHeureReelle"])
|
if end < today:
|
||||||
if end.astimezone(pytz.timezone('Europe/Paris')).strftime("%d %H:%M") != new_end.strftime("%d %H:%M"):
|
continue
|
||||||
evt["new_end"] = new_end
|
if toofar is not None and start > toofar:
|
||||||
|
continue
|
||||||
if len(train_conjoncturels) > 0:
|
|
||||||
evt["alert"] = train_conjoncturels["messagesConjoncturels"][0]["titre"]
|
|
||||||
elif len(train_situations) > 0:
|
|
||||||
evt["alert"] = train_situations[0]["libelleSituation"]
|
|
||||||
|
|
||||||
if new_start > datetime.now(tz=pytz.timezone('Europe/Paris')):
|
|
||||||
if "evenement" in train_start_station["depart"]:
|
|
||||||
evt["alert"] = train_start_station["depart"]["evenement"]["texte"]
|
|
||||||
if "voie" in train_start_station and "numero" in train_start_station["voie"]:
|
|
||||||
evt["info"] = "Départ voie " + train_start_station["voie"]["numero"]
|
|
||||||
if place is not None:
|
|
||||||
evt["info"] = ((evt["info"] + "\n") if "info" in evt else "") + place
|
|
||||||
elif train_end_station is not None:
|
|
||||||
evt["info"] = "Arrivée à " + new_end.strftime("%H:%M")
|
|
||||||
if "evenement" in train_end_station["arrivee"]:
|
|
||||||
evt["alert"] = train_end_station["arrivee"]["evenement"]["texte"]
|
|
||||||
evt["info"] += " (+%d')" % train_end_station["arrivee"]["evenement"]["retard"]["duree"]
|
|
||||||
if "voie" in train_end_station and "numero" in train_end_station["voie"]:
|
|
||||||
evt["info"] += " voie " + train_end_station["voie"]["numero"]
|
|
||||||
|
|
||||||
events.append(evt)
|
|
||||||
elif now is not None and component.decoded("DTEND") < now:
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
evt = {
|
if isinstance(end, date) and end < todayd:
|
||||||
"summary": component.get("SUMMARY"),
|
continue
|
||||||
"start": component.decoded("DTSTART"),
|
if toofard is not None and start > toofard:
|
||||||
"end": component.decoded("DTEND"),
|
continue
|
||||||
}
|
|
||||||
|
|
||||||
if "location" in component:
|
events.append(component)
|
||||||
evt["location"] = component.decoded("LOCATION").decode()
|
|
||||||
|
|
||||||
events.append(evt)
|
|
||||||
|
|
||||||
# Sort events
|
# Sort events
|
||||||
events.sort(key=lambda e: e["start"])
|
events.sort(key=lambda e: time.mktime(e.decoded("DTSTART").timetuple()))
|
||||||
|
|
||||||
return events
|
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)
|
||||||
|
if train_start_station is not None:
|
||||||
|
start = component.decoded("DTSTART")
|
||||||
|
end = component.decoded("DTEND")
|
||||||
|
evt = {
|
||||||
|
"summary": component.get("SUMMARY"),
|
||||||
|
"start": start,
|
||||||
|
"end": end,
|
||||||
|
}
|
||||||
|
|
||||||
|
new_start = datetime.fromisoformat(train_start_station["depart"]["dateHeureReelle"])
|
||||||
|
if start.astimezone(pytz.timezone('Europe/Paris')).strftime("%d %H:%M") != new_start.strftime("%d %H:%M"):
|
||||||
|
evt["new_start"] = new_start
|
||||||
|
|
||||||
|
if train_end_station:
|
||||||
|
new_end = datetime.fromisoformat(train_end_station["arrivee"]["dateHeureReelle"])
|
||||||
|
if end.astimezone(pytz.timezone('Europe/Paris')).strftime("%d %H:%M") != new_end.strftime("%d %H:%M"):
|
||||||
|
evt["new_end"] = new_end
|
||||||
|
|
||||||
|
if len(train_conjoncturels) > 0:
|
||||||
|
evt["alert"] = train_conjoncturels["messagesConjoncturels"][0]["titre"]
|
||||||
|
elif len(train_situations) > 0:
|
||||||
|
evt["alert"] = train_situations[0]["libelleSituation"]
|
||||||
|
|
||||||
|
if new_start > datetime.now(tz=pytz.timezone('Europe/Paris')):
|
||||||
|
if "evenement" in train_start_station["depart"]:
|
||||||
|
evt["alert"] = train_start_station["depart"]["evenement"]["texte"]
|
||||||
|
if "voie" in train_start_station and "numero" in train_start_station["voie"]:
|
||||||
|
evt["info"] = "Départ voie " + train_start_station["voie"]["numero"]
|
||||||
|
if place is not None:
|
||||||
|
evt["info"] = ((evt["info"] + "\n") if "info" in evt else "") + place
|
||||||
|
elif train_end_station is not None:
|
||||||
|
evt["info"] = "Arrivée à " + new_end.strftime("%H:%M")
|
||||||
|
if "evenement" in train_end_station["arrivee"]:
|
||||||
|
evt["alert"] = train_end_station["arrivee"]["evenement"]["texte"]
|
||||||
|
evt["info"] += " (+%d')" % train_end_station["arrivee"]["evenement"]["retard"]["duree"]
|
||||||
|
if "voie" in train_end_station and "numero" in train_end_station["voie"]:
|
||||||
|
evt["info"] += " voie " + train_end_station["voie"]["numero"]
|
||||||
|
|
||||||
|
elif now is not None and time.mktime(component.decoded("DTEND").timetuple()) < time.mktime(now.timetuple()):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
evt = {
|
||||||
|
"summary": component.get("SUMMARY"),
|
||||||
|
"start": component.decoded("DTSTART"),
|
||||||
|
"end": component.decoded("DTEND"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if "location" in component:
|
||||||
|
evt["location"] = component.decoded("LOCATION").decode()
|
||||||
|
|
||||||
|
if evt is not None:
|
||||||
|
yield evt
|
||||||
|
|
||||||
def is_train_event(self, evt):
|
def is_train_event(self, evt):
|
||||||
if "description" not in evt:
|
if "description" not in evt:
|
||||||
return None, None, None, None, None
|
return None, None, None, None, None
|
||||||
@ -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")
|
||||||
|
|
||||||
draw.text(
|
if isinstance(evt["start"], datetime):
|
||||||
(2, align),
|
draw.text(
|
||||||
evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M") + " " + evt["summary"],
|
(2, align),
|
||||||
fill="black", anchor="lt", font=fnt_R
|
evt["start"].astimezone(pytz.timezone('Europe/Paris')).strftime("%H:%M") + " " + evt["summary"],
|
||||||
)
|
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),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user