#!/usr/bin/env python3 # File: agenda.py # Location: https://bgstack15.ddns.net/cgit/agenda # Author: bgstack15 # Startdate: 2023-05-16-3 14:09 # Title: Generate daily agenda html message # Purpose: remind myself of what is happening today # History: # Usage: See function summarize() # Reference: # 1. basic_usage_examples.py from caldav # 2. https://stackoverflow.com/questions/29643352/converting-hex-to-rgb-value-in-python # 3. https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color # Improve: # Dependencies: # python3-caldav import sys, caldav from datetime import date, datetime, timedelta def get_calendar_color(calendar): """ Author: bgstack15 Purpose: return the hexadecimal color (and alpha channel if present) for the given caldav calendar. """ color = prop = None try: colorprop = caldav.elements.ical.CalendarColor() prop = calendar.get_properties(props=[colorprop]) if prop: for j in prop: # only the one color = prop[j] except Exception as e: print(f"Debug: no color info available for calendar {calendar}, e: {e}") return color def print_calendars_demo(calendars): """ This example prints the name and URL for every calendar on the list """ if calendars: ## Some calendar servers will include all calendars you have ## access to in this list, and not only the calendars owned by ## this principal. print("your principal has %i calendars:" % len(calendars)) for c in calendars: color = get_calendar_color(c) print("Name: %-36s Color: %-9s URL: %s" % (c.name, color, c.url)) else: print("your principal has no calendars") def event_starts_during_date(eventdate, thisdate): # eventdate must be a date or datetime # thisdate MUST be date, not datetime happens_on_this_date = False if type(eventdate) == datetime: eventdate = eventdate.date() if eventdate >= thisdate and eventdate < (thisdate + timedelta(days=1)): happens_on_this_date = True #print(f"Checking eventdate {eventdate} to see if it happens on thisdate {thisdate}: {happens_on_this_date}") return happens_on_this_date def show_event(event,thisdate): # only display event if it is on thisdate. if type(thisdate) == datetime: thisdate = thisdate.date() if type(thisdate) != date: print("Warning! Setting thisdate to today.") thisdate = date.today() response = "" all_dtstart_dt = [] all_dtstart_str_dt = [] #print(event.data) #for i in event.icalendar_component: # print(f"{i} IS {event.icalendar_component[i]}") dtstart = event.icalendar_component["DTSTART"] dtstart_dt = dtstart and dtstart.dt if str(dtstart_dt) not in all_dtstart_str_dt and event_starts_during_date(dtstart_dt, thisdate): #print(f"Event dtstart_dt is {dtstart_dt}") all_dtstart_dt.append(dtstart_dt) all_dtstart_str_dt.append(str(dtstart_dt)) if all_dtstart_dt: for i in all_dtstart_dt: summary = event.icalendar_component["SUMMARY"].strip() thishour = int(i.strftime("%I")) thistime = str(thishour) + i.strftime(":%M%P") if type(event.icalendar_component["DTEND"].dt) == date: thistime = "All day" response += f"{thistime}: {summary}
\n" return response # From reference 2 def hex_to_rgb(value): value = value.lstrip('#').rstrip(";") lv = len(value) return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) def rgb_to_hex(rgb): return '#%02x%02x%02x' % rgb # bgstack15 def get_faded_color(hexcolor): """ Given hexcolor as string of '#ffccaa', return a faded color """ outcolor = "" try: r, g, b = hex_to_rgb(hexcolor) except: try: r, g, b, a = hex_to_rgb(hexcolor) except: print(f"Warning! Cannot understand color {hexcolor} for getting a faded value.") return hexcolor # so now we have r, g, b. We do not know yet how to use alpha. if len(hexcolor) == 4: # that is, if hexcolor is "#fff" or similar r = r * 16 g = g * 16 b = b * 16 r1 = int((255 + r)/2) g1 = int((255 + g)/2) b1 = int((255 + b)/2) outcolor = rgb_to_hex((r1,g1,b1)) return outcolor # Adapted from reference 3 def get_text_color_based_on_background_color(bgColor, lightColor, darkColor): color = bgColor.lstrip("#").rstrip(";") r, g, b = hex_to_rgb(color) uicolors = [r/255, g/255, b/255] adjusted = [] for col in uicolors: col2 = col if col <= 0.03928: col2 = col/12.92 col2 = pow((col2 + 0.055)/1.055,2.4) adjusted.append(col2) L = (0.2126 * adjusted[0] + 0.7152 * adjusted[1] + (0.072 * adjusted[2])) return darkColor if L > 0.179 else lightColor #function pickTextColorBasedOnBgColorAdvanced(bgColor, lightColor, darkColor) { # var color = (bgColor.charAt(0) === '#') ? bgColor.substring(1, 7) : bgColor; # var r = parseInt(color.substring(0, 2), 16); // hexToR # var g = parseInt(color.substring(2, 4), 16); // hexToG # var b = parseInt(color.substring(4, 6), 16); // hexToB # var uicolors = [r / 255, g / 255, b / 255]; # var c = uicolors.map((col) => { # if (col <= 0.03928) { # return col / 12.92; # } # return Math.pow((col + 0.055) / 1.055, 2.4); # }); # var L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]); # return (L > 0.179) ? darkColor : lightColor; #} def event_key(i): return i.icalendar_component["DTSTART"].dt def summarize_calendar(calendar, thisdate = None, count = 0): """ Examine calendar for given thisdate (of type datetime.date) """ response = "" #d = date.today()+timedelta(days=-1) if type(thisdate) == datetime: thisdate = thisdate.date() if type(thisdate) != date: thisdate = datetime.today() # so now, thisdate should be of type datetime.date d = thisdate d2 = d + timedelta(days=1) #print(f"Using d {d} and d2 {d2}") year, month, day = d2.year, d2.month, d2.day events_fetched = calendar.search( start=d, end=datetime(year,month,day), event=True, expand=True, ) x = 0 events_fetched = [i for i in events_fetched if event_starts_during_date(i.icalendar_component["DTSTART"].dt,d)] events_wholeday = [i for i in events_fetched if type(i.icalendar_component["DTSTART"].dt) == date] events_regular = [i for i in events_fetched if type(i.icalendar_component["DTSTART"].dt) != date] events_sorted = sorted(events_regular, key=event_key) for i in events_wholeday + events_sorted: x = x + 1 if 1 == x: if 1 == count: datestring = thisdate.strftime("%B %d") response += f"

Summary for {datestring}

\n" color = get_calendar_color(calendar) color2 = get_faded_color(color) h2_text_color = get_text_color_based_on_background_color(color,"#ffffff","#000000") text_color = get_text_color_based_on_background_color(color2,"#ffffff","#000000") response += f"

{calendar.name}

\n" response += show_event(i,thisdate) if x > 0: response += "
\n" return response def summarize(thisdate = None, url = "http://example.com/radicale/", username = "username", password = "insecure"): """ Given a date ("YYYY-MM-DD" or datetime.date object), produce a string of html suitable for an agenda email. Example: with open("agenda.html","w") as w: w.write(summarize("2023-05-14",url="https://server3.ipa.example.com/radicale/",username="username",password="password")) """ response = "" if type(thisdate) == str: try: thisdate = datetime.strptime(thisdate,"%Y-%m-%d") except: print("Warning! Expected YYYY-MM-DD or a datetime.date or datetime.datetime object. Using today's date.") thisdate = datetime.today() if type(thisdate) == datetime: thisdate = thisdate.date() elif thisdate is None: thisdate = date.today() if type(thisdate) != date: print(f"Warning! Unknown date: \"{thisdate}\". Using today.") thisdate = date.today() with caldav.DAVClient(url=url, username=username, password=password) as client: my_principal = client.principal() calendars = my_principal.calendars() #print_calendars_demo(calendars) x = 0 for cal in calendars: x = x + 1 response += summarize_calendar(cal, thisdate, x) return response