aboutsummaryrefslogtreecommitdiff
#!/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}<br/>\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"<h1>Summary for {datestring}</h1>\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"<div style='background-color: {color2}; color: {text_color}'><h2 style='background-color: {color}; color: {h2_text_color}'>{calendar.name}</h2>\n"
      response += show_event(i,thisdate)
   if x > 0:
      response += "</div>\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
bgstack15