From 24b1b939c6eb50e0628a8f26e467fbd309b7cad6 Mon Sep 17 00:00:00 2001 From: pepev-nrt Date: Sat, 1 Feb 2025 22:47:52 +0100 Subject: [PATCH] calendar scripts working --- assets/output.bmp | Bin 3966 -> 3966 bytes cal/cal.py | 90 +++++++++++++++++++++++++++++++++++ config.json | 7 ++- raspagenda.py | 117 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 189 insertions(+), 25 deletions(-) diff --git a/assets/output.bmp b/assets/output.bmp index 18d689d2ae55a3536652881b0bb7c5bbc38ec0f6..38751e3d3ee690a3072e48e1274a103a568bb770 100644 GIT binary patch literal 3966 zcmbuC!Ee)69LHhR;KI@aSC$;uwB2)95v87X;k5Aw?7*Z=<$%@}ZKB&~V-V7^NoX9# zB~qvlJM1O3z2_u$lETn_mgD+;-tYVS zz4v?1c0c$qGkHMR41V9m`5fmrI1l1rdmsF6LY}YTo=-N4n-RYiccn<%%lYBfg)O~! z!K&;vw<_np>yA}$=gmU#naR91`?bd0Cgy=9dDAKsoiV&E zc*Sj&D@8}>*WQ_8|LGkvvWxAerL8-L=h~)MbbO(Icq+xbBlI2f*1Gec;66cossGXG z6US8l)O5Ga@!MOs@67$R;r_F=k#M%yKXERXQ@YbLna`LL5md=liF91Se_$TW%*=ux zZiVf8K!N1OWPVyDWo7~TqxNd^8~S$#I%E9cN>Fa9yja#lZbm5L2lD|IQ%Cnq@We^S@i)2+ zJ(~)9mB=DhCsO@@jNs{}<1`AMsb6dkcmQ!yJ;~=BQ}7UDd{)h7esV?l3*OK#P7eIn zHo-I292ws}-4xu{qWb55{fTJ#mp`v_e5rpc&fyQv`A@Ah8XK*x+mZa8iGwQ9og=S< z%dD}YcpnKX(JdrLfsSp(Tx^;cU))MsDABb`suB7`FQ|{@G|98nfWkHl`WliC z*T+W>4%kF|wxRlvquMIbZ*&t})@RT0-Q=562ldrf;8o0^{$YLGIe+)5DRoMueUicY zT)u+!6V9LO_^W=Ot9Uq&?^CzWTXX*E8-2b+mcc_%m0o?IzuMQw{^e`-cwKgzpE zZin|bVf?Z_dyVdyLH;12_39&TE!h}*ehlifpLl*{_K58qob zE3!QnzgM5lwK=mep7Z+NeHq_RKiogN)>_8oMFks<|3dw+KJDp+c4cFXKjwefV*1hg zgdO92*n23yQ5?_z|MGmjiX)~H%P(JLdu85%$2pwFKKgh$1#8T2bgMW<^s$9}ZZs~B zj~{sSOz`g-WgH{^v|JL^Hu5nfZp8R>Md2Ip-$i5ONAM;2=)wAaq(&J3DR_ZgUJi2o z$2$m)m#_W;?&IanUizivCCvcn|mHBlo-ywVr zNFI!E(gRZQO(J*)@X%{izX1PH6aHuAYimCqH1D0#;R+r;l?FQO?>BYR zk~}vcboqc1jvw{q4-+F-;icX)mClCTlkZa_mn-)mUO7bbA%93UZU>L(2lwAz47sJq z`VaFql_$kI=ZEL*NVJK-hK&`ry6bBiC4bEsABHfs8LfEcF<#l&_Y{ z6{FRqnPW-S&T7@kvR(T^)R)JHnsM2-muMQ_?RJ`~IBII2cLu!M=YKHAIN)C?=gRPZ z1pg!8N4p8NN`fOZaIKSr1l2?RiLRl99G~2S_>aE~jv)Te$AdGPO+)m2!o) z7m$C7vT1G7DAQdY^(uW1PNWeEy!{m^U80RNUF*%oy#t>zw*|-euY=oJs-@T&wky{+ Xf7*<-kdI~D$p13P{k?CO|G@tRPpnR_ literal 3966 zcmeH~PjB2r7{=2gk_%d&k*efER6arxX^c3~6MTUlibS=0h-lzKfb8xx>In(K0S;Ly zjSsz-11bSk#4CY{a*1znpuv$U)d!@wuz=&*o`+{_Z;~bR8U*6jS&cn2zj^=6yfZuF zmtXBWQ)OD@^LggS%`7G-un}o>}OjgU<@I>3c<3X$IB2Be|U4FOy zp4ZG(Ge7To$n$+s{|veAK9u-tmo6+QEd2Vm;9sjVNL_5cdsW~cRlF>MGK52!**TvItzCi30L@E12F`K=$x`{6Z-FWtEX^lpDI%wM>4hw|it z{J~Ra6!7IUPYe7rItT5e5ctZ2*^nXna8(KM2@hgqp}Ol^IT&BAZpJsT^MXBqggxy1 zKt=TbE8G-#>}^thyD!Dxd%e%~*;|wLhf;7+6vzerU4(%df^x0?aeqc5vp+ri_;NZo zGW#2LPB2bZ{bR`Y6MkYc>f(PtACGlDncw5N7?=LZ{HE;OVV35Phm+oF;M_umu_wvv z48J#V^oHgyM3jf`x{bVBiAwfy7s+=``g!>J4ZPst-Dp1^UTowcegZF8l7D$v93YPx z#;>6oc|ksEkiQwIJZ<31Tk5y`7s)?t|FCrFk-pl*)!{>#-=98<{E@8xin$`l@z?Ra zi+%$yVt+3@&96_nK)L=IU*2*4I(~&KHP3i_o#J$9x#st=YvDWdD>V;YOZ@fD{A$e? zhnDq2{k@874XteUdGHzkc{%gF(|l*%S0(5E3{H=a_JSzpS`bQ}y(!SIwhV$8p>>FMnZU z0W1x0BiJyrQS0Arx7!_fUOE_fnxz4M8%D!1=LMNO9YA-Yl7Cuqj)UW;{EWl9!Qr#( zbNYOAc#b_P&Q29?D@0MLeX0Se2miYMNXnl`cCf|R@%_l>( Rzb54-YUTc$Xql`G{{$5l1$F=c diff --git a/cal/cal.py b/cal/cal.py index e69de29..21dba7c 100644 --- a/cal/cal.py +++ b/cal/cal.py @@ -0,0 +1,90 @@ +#!/usr/bin python3 +# -*- coding: utf-8 -*- +""" +# TODO: Comment this +""" + +import caldav +from datetime import date, datetime, timedelta +import pytz +import vobject + + +class CalendarHelper: + + def __init__(self, caldavURL: str, caldavUser: str, caldavPassword: str, timezone="Europe/Madrid", caldavBlacklist=[""]): + self.caldavURL = caldavURL + self.caldavUser = caldavUser + self.caldavPassword = caldavPassword + self.caldavBlacklist = caldavBlacklist + self.timezone = timezone + + + def fetch_cals(self): + + with caldav.DAVClient( + url=self.caldavURL, + username=self.caldavUser, + password=self.caldavPassword + ) as client: + + principal = client.principal() + + # Get user's calendars + calendars = principal.calendars() + + # Upcoming events on all calendars + tz = pytz.timezone(self.timezone) + now = datetime.now(tz=tz) + end = now + timedelta(days=30) + all_events = [] + for calendar in calendars: + #print(str(calendar)) + if str(calendar) not in self.caldavBlacklist: + events = calendar.search(start=now, end=end, expand=True) + all_events.extend(events) + + + # Parse events into parsed_vents list + parsed_events = [] + for event in all_events: + parsed_events.append(self.parse_event(event.data)) + + for event in parsed_events: + # If datetime.datetime, do nothing, if datetime.time, convert it to datetime.datetime + if not isinstance(event["DTSTART"], datetime): + event["DTSTART"] = date.strftime(event["DTSTART"], '%Y-%m-%d, %H:%M:%S') + event["DTSTART"] = datetime.strptime(event["DTSTART"], '%Y-%m-%d, %H:%M:%S') + + else: + event["DTSTART"] = datetime.strftime(event["DTSTART"], '%Y-%m-%d, %H:%M:%S') + event["DTSTART"] = datetime.strptime(event["DTSTART"], '%Y-%m-%d, %H:%M:%S') + + + #event["DTSTART"] = datetime.combine(event["DTSTART"], datetime.min.time()) + + #print(parsed_events) + # Sort the list of dictionaries by DTSTART + sorted_events = sorted(parsed_events, key=lambda x: x['DTSTART']) + + #return sorted_events + + # Print the sorted list of events + #for event in sorted_events: + #print(f'{event["DTSTART"]} --- {event["SUMMARY"]}') + + return sorted_events + + + def parse_event(self, event_str: str): + calendar = vobject.readOne(event_str) + event = calendar.vevent + + event_dict = {} + for component in event.getChildren(): + try: + event_dict[component.name] = component.value + except: + continue + + return(event_dict) \ No newline at end of file diff --git a/config.json b/config.json index 1df5663..ed44d6d 100644 --- a/config.json +++ b/config.json @@ -12,5 +12,10 @@ "latitude": 40.4084, "longitude": -3.6876, "timezone": "Europe/Madrid", - "locales": "es_ES.UTF-8" + "locales": "es_ES.UTF-8", + + "caldavURL": "", + "caldavUser": "", + "caldavPassword": "", + "caldavBlacklist": "blocked cal 1, blocked cal 2" } diff --git a/raspagenda.py b/raspagenda.py index 8490131..231a631 100644 --- a/raspagenda.py +++ b/raspagenda.py @@ -18,6 +18,7 @@ import json import logging from PIL import Image,ImageDraw,ImageFont +from cal.cal import CalendarHelper from weather.weather import WeatherHelper @@ -29,19 +30,29 @@ def main(): isDisplayConected = config['isDisplayConected'] # set to true when debugging rendering without displaying to screen + screenWidth = config['screenWidth'] # Width of E-Ink display. Default is landscape. Need to rotate image to fit. screenHeight = config['screenHeight'] # Height of E-Ink display. Default is landscape. Need to rotate image to fit. imageWidth = config['imageWidth'] # Width of image to be generated for display. imageHeight = config['imageHeight'] # Height of image to be generated for display. rotateAngle = config['rotateAngle'] # If image is rendered in portrait orientation, angle to rotate to fit screen + hourFormat = config['hourFormat'] # The format the hour will be displayed. eg. 13:02 or 01:02 PM + latitude = config['latitude'] # A float. The latitude for the Weather API. longitude = config['longitude'] # A float. The longitude for the Weather API. timezone = config['timezone'] # The timezone is necesary for the Weather API - locales = config['locales'] # Set the locales for the month name + locales = config['locales'] # Set the locales for the month name + + caldavURL = config['caldavURL'] # URL to Caldav calendar. eg. 'https://nextcloud.example/remote.php/dav' + caldavUser = config['caldavUser'] # Caldav username + caldavPassword = config['caldavPassword'] # Caldav password + caldavBlacklist = config ['caldavBlacklist'] # Caldav list of calendars to be ommited + # Convert json "str1, str2" to a python list + caldavBlacklist = caldavBlacklist.split(',') + for i in range(len(caldavBlacklist)): + caldavBlacklist[i-1] = caldavBlacklist[i-1].strip() - weatherService = WeatherHelper(latitude, longitude, timezone) - weather_data = weatherService.fetch_open_meteo_data() locale.setlocale(locale.LC_TIME, locales) @@ -52,6 +63,8 @@ def main(): time = dt.datetime.now().strftime("%H:%M") day = dt.datetime.now().strftime("%A, %d %b") + tomorrow = dt.datetime.now() + dt.timedelta(days=1) + tomorrow = dt.datetime.strftime(tomorrow, "%A, %d %b") assets = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'assets') @@ -67,6 +80,7 @@ def main(): image = Image.new('1', (imageWidth, imageHeight), 255) # 255: clear the frame draw = ImageDraw.Draw(image) + # Draw todays date draw.text((0, 0), f"{day}. {time}", font = font16_bold, fill = 0) # draw the current time in the top left corner # Frames @@ -77,42 +91,97 @@ def main(): # Calendar icon cal_coordinates_x = 207 cal_coordinates_y = 2 - cal_icon = Image.open(os.path.join(assets, "cal-icon3.bmp")) # calendar icon # This seems complicates, but it just draw a white rectangle bellow the calendar draw.rectangle([(cal_coordinates_x-2, cal_coordinates_y),(cal_coordinates_x + 28, cal_coordinates_y + 30)], fill = 255) image.paste(cal_icon, (cal_coordinates_x, cal_coordinates_y)) # LEFT SEGMENT (WEATHER) - # Todays information - today_icon = weatherService.iconize_weather(weather_data["current_weather_code"]) - today_icon = Image.open(os.path.join(weather_icons, today_icon)) - image.paste(today_icon, (29,20)) + try: + # Todays information + weatherService = WeatherHelper(latitude, longitude, timezone) - today_temperature = f"{str(weather_data['current_temperature_2m'])}°" - draw.text((66,28), today_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon + weather_data = weatherService.fetch_open_meteo_data() + today_icon = weatherService.iconize_weather(weather_data["current_weather_code"]) + today_icon = Image.open(os.path.join(weather_icons, today_icon)) + image.paste(today_icon, (29,20)) + + today_temperature = f"{str(weather_data['current_temperature_2m'])}°" + draw.text((66,28), today_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon - #draw.text((160, 26), 'Agenda', font = font16_bold, fill = 0) + #draw.text((160, 26), 'Agenda', font = font16_bold, fill = 0) - # All the tomorrows information - tomorrow_icon = weatherService.iconize_weather(weather_data["tomorrow_weather_code"]) - tomorrow_icon = Image.open(os.path.join(weather_icons, tomorrow_icon)) - image.paste(tomorrow_icon, (6,74)) # The icon is in the bottom left corner + # All the tomorrows information + tomorrow_icon = weatherService.iconize_weather(weather_data["tomorrow_weather_code"]) + tomorrow_icon = Image.open(os.path.join(weather_icons, tomorrow_icon)) + image.paste(tomorrow_icon, (6,74)) # The icon is in the bottom left corner - tomorrow_temperature = f"{str(weather_data['tomorrow_temperature_2m_max'])}/{str(weather_data['tomorrow_temperature_2m_min'])}°" - draw.text((6,106), tomorrow_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon + tomorrow_temperature = f"{str(weather_data['tomorrow_temperature_2m_max'])}/{str(weather_data['tomorrow_temperature_2m_min'])}°" + draw.text((6,106), tomorrow_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon - # All the day after tomorrows information - day_after_icon = weatherService.iconize_weather(weather_data["day_after_weather_code"]) - day_after_icon = Image.open(os.path.join(weather_icons, day_after_icon)) - image.paste(day_after_icon, (52,74)) + # All the day after tomorrows information + day_after_icon = weatherService.iconize_weather(weather_data["day_after_weather_code"]) + day_after_icon = Image.open(os.path.join(weather_icons, day_after_icon)) + image.paste(day_after_icon, (52,74)) - day_after_temperature = f"{str(weather_data['day_after_temperature_2m_max'])}/{str(weather_data['day_after_temperature_2m_min'])}°" - draw.text((52,106), day_after_temperature, font = font16_bold, fill = 0) + day_after_temperature = f"{str(weather_data['day_after_temperature_2m_max'])}/{str(weather_data['day_after_temperature_2m_min'])}°" + draw.text((52,106), day_after_temperature, font = font16_bold, fill = 0) + except: + pass + # TODO: desing an error icon and # RIGHT SEGMENT (CALENDAR/AGENDA) - # TODO: writing this part and also the logic part + try: + calService = CalendarHelper(caldavURL, caldavUser, caldavPassword, timezone=timezone, caldavBlacklist=caldavBlacklist) + calendar_data = calService.fetch_cals() + #print(calendar_data) + + last_date: str = None + max_agenda_lines = 7 + agenda_lines = 0 + + agenda_x = 96 + agenda_y = 16 + font_size = 13 + + for event in calendar_data: + text: str = '' + bold: bool = False + + event_date = dt.datetime.strftime(event["DTSTART"], "%A, %d %b") + if event_date != last_date: + + last_date = event_date + # check if date is today or tomorrow + if last_date == day: + text = "Hoy" + bold = True + elif last_date == tomorrow: + text = "Mañana" + bold = True + else: + text = last_date + bold = True + + coords_y = font_size * agenda_lines + agenda_y + if coords_y >= screenHeight - font_size*2: + break + draw.text((agenda_x, coords_y), text, font = font16_bold, fill = 0) + agenda_lines += 1 + last_date = event_date + + text = f'-{event["SUMMARY"]}' + + coords_y = font_size * agenda_lines + agenda_y + if coords_y >= screenHeight - font_size: + break + draw.text((agenda_x, coords_y), text, font = font16, fill = 0) + agenda_lines += 1 + + except: + pass + # TODO: clean the code and comment # draw.line([(0,50),(50,0)], fill = 0,width = 1) # draw.chord((10, 60, 50, 100), 0, 360, fill = 0)