Help Six Degrees Edinburgh with a speciality coffee survey

Skip to main content

The Thermal Printer Project: How I Print Events

Written by . Published on under the IndieWeb category.

A piece of thermal printer containing some example calendar events

A key part of my “daily update” on the thermal printer is the daily calendar. This calendar shows all of the events I have in a day, as well as any tasks that I have added to Google Calendar as an all-day event. I also display all the birthdays and holidays in my daily update using some of the same code I use to retrieve my daily update. Birthdays and holidays are only printed on Mondays.

To print events, birthdays, and holidays, I had two questions I had to consider.

  1. How would I access events?
  2. How would I process different calendars without repeating code? This question came later in the development process but was important in my writing clean, readable code.

Let me talk about them one-by-one.

How would I access events?

Google Calendar lets you create links to .ics files which contain your events. You can make some links public but you can also use a private link to your calendar provided by Google, which is what I opted to use. This link does not require any authentication: you just need to know the link, which does not follow an obvious URL structure. This is ideal because it meant I did not need to think about API keys and tokens.

Being able to access my calendar, the next step was to write a program that read events from my calendar and translated them into print events for my thermal printer. “events” means daily events, birthdays, and holidays. My program roughly does the following:

  1. Prints a message informing me the type of events that will follow (calendar, holidays, or birthdays).
  2. Retrieves events from the Google Calendar .ics file links I provide.
  3. Saves those events into a local calendar file.
  4. Uses the icalendar library to read events.
  5. Finds events on a day / in the next week.
  6. Creates a list of event names and the date / time the event starts.
  7. Prints those events to the console (or a message saying I have no events / Birthdays / holidays)

I wrote a function that does all of this (alongside a helper function, daterange), which you can see below.

import requests
import icalendar
import datetime
import main
import recurring_ical_events

today = datetime.date.today()

def daterange(date1, date2):
	# I found this snippet on the internet but have forgotten the source.
    for n in range(int ((date2 - date1).days)+1):
        yield date1 + datetime.timedelta(n)

def retrieve_calendar_information(url, header_message, file_to_open, cal_type, printer):
    main.print_header(header_message)

    r = requests.get(url)

    open(file_to_open, "wb").write(r.content)

    calendar = open(file_to_open, "rb")

    cal = icalendar.Calendar.from_ical(calendar.read())

    events = []

    start = today - datetime.timedelta(days=today.weekday())
    end = start + datetime.timedelta(days=6) # Monday is 0 so days=6 means all week

    this_week = daterange(start, end)

    this_week_list = [str(i) for i in this_week]

    # Use this to show all recurring events
    recurring_events = recurring_ical_events.of(cal).between(start, end)

    for component in recurring_events:
        if component.name == "VEVENT":
            if cal_type == "Holidays" or cal_type == "Birthdays":
                if component.decoded("dtstart").strftime("%Y-%m-%d") in this_week_list:
                    events.append({
                        "summary": str(component.get("summary")),
                        "date_time": "{}".format(component.decoded("dtstart").strftime("%B %d, %Y"))
                    })
            else:
                if component.decoded("dtstart").strftime("%B %d, %Y") == today.strftime("%B %d, %Y"):
                    events.append({
                        "summary": str(component.get("summary")),
                        "date_time": "{}".format(component.decoded("dtstart").strftime("%B %d, %Y (%H:%M %p)"))
                    })

    if len(events) > 0:
        # Reverse events so they appear in order from start of day / week to end of day / week
        events.reverse()
        for e in events:
            printer.println(main.textWrapped("{} - {}".format(e["summary"], e["date_time"])))

This code does exactly what I described earlier. I retrieve a calendar, save it locally, open that file, find events (using the cal.walk() method), and then print those events (which are classed under VEVENT components in the calendar). I have a separate if statement that processes holidays and birthdays so that I only retrieve holidays and birthdays in the next week rather than on the day the program is executed. I only run the function above for holidays and birthday events on Mondays, which is controlled in the main printer program (not shown in this article). I have three separate functions which call the function above, one for birthdays, holidays this week, and my daily calendar.

I am using a library called recurring_ical_events to make sure that recurring events show on my calendar. While testing my code, I realised that the icalendar module does not handle recurring events well. So I defer all of that to the recurring_ical_events library. This library creates new events that are repeats of previous events. This line of code handles processing recurring events:

recurring_events = recurring_ical_events.of(cal).between(start, end)

With the code snippets above, I can print out all of the events in my calendar, including those which are recurring.

Birthdays and holidays appear differently on print-outs to my daily events. Daily events show the time at which the event is due to start, whereas birthdays and holidays do not. However, I do need to add end dates to my events. I had not thought about this yet. Honestly, I usually know when events are going to end in my head because I often look at my schedule in advance anyway and many events on my calendar (especially for work) are recurring, but this would be a nice feature to have anyway.

The last part of the loop reverses events. The events are in reverse-chronological order by default so I need to reverse them so that the first events of the day appear at the top of a list (relevant for my daily schedule, not so much for holidays and birthdays). I then iterate over all events found in my calendar for a given day / week and print them using my thermal printer. I print both the event summary and the date / time the event is scheduled.

How to process different calendars

So, how do I display these different calendars? The answer is through three functions that control each of the three calendars my program handles: my birthdays, holidays, and daily schedule. Each of these calendars all appear on my Google Calendar but have separate links because they individual calendars in the app that all appear on one master calendar. Here’s an example of a function that calls the retrieve_calendar_information() function:

def mycal(printer):
    url = "URL"

    retrieve_calendar_information(url, "Your schedule for the day", "mycalendar.ics", "Events", printer)

This function passes the following information to the function featured in the first code snippet:

  1. The URL of the calendar to retrieve.
  2. The message to print on the thermal printer before printing events.
  3. The file name that should be assigned to the calendar that I save.
  4. The type of calendar I am retrieving (used in the if statement I discussed earlier).
  5. The printer object, which controls my printer.

The three functions I wrote are identical to this one in structure but have different parameters and URLs.

Wrapping up (and why I wrote this post)

This post is a bit heavier in code than my other ones. That’s because I found there was not a lot of information out there about how to read a calendar in Python. I created this post to share the code you would need to ingest your calendar into a Python program and also to share another module I built to add functionality to my thermal printer.

This program was fun to write and serves a real purpose: keeping me updated on events. I have been reading the daily schedule regularly since I wrote this program. There was a birthday in the family after I wrote the script, which showed up on this week’s Monday birthday schedule. This was lucky because I was able to test the birthday script with a real example.

I hope this post was interesting and/or useful to you. To wrap things up, I’d like to show you a daily schedule that my printer has prepared (birthdays and holidays not pictured):

Also posted on IndieNews.

Go Back to the Top