Upcoming meetings on an epaper display

How I learned to love Python

epaper display on a Raspberry Pi Zero I really got interested in epaper recently and wanted to do a project with an epaper display. Using an extra Raspberry Pi Zero W I had lying around, I was able write a little Python to display upcoming meetings from my Google Calendar.

The Screen

If you search for epaper displays for a Raspberry Pi, one of the top results will be for Waveshare. They offer a bunch of different sizes with black & white and color options. I went with the 2.13inch, 2 color display since it matched the size of the Raspberry Pi Zero W perfectly. The physical installation was one step, slide the GPIO header pins on the Pi into the display.

For the Raspberry Pi, I used the 32-bit Raspberry Pi OS Bullseye image. I followed the setup instructions on the Waveshare wiki and successfully ran the demo. The screen sprang to life, displayed some text and images and shut off.

Accessing the calendar

The next step was accessing my calendar. Most epaper calendar projects required either an ical feed or the Google Calendar API. My work calendar doesn’t allow either, so I had to look elsewhere.

I found gcalendar, a command line tool that can read your Google Calendar. To authenticate it launches a browser and starts the standard Google authentication workflow for a third party app. Once authorized, I tested the app in the command line and it returned a list of events.

pi@PiZero:~ $ gcalendar --calendar WorkCal --no-of-days 1
2022-03-11:09:30 - 2022-03-11:10:00 Just a test meeting 
2022-03-11:10:30 - 2022-03-11:11:00 Another test meeting later in the day 
2022-03-11:14:00 - 2022-03-11:14:30 Test afternoon meeting this has a pretty long title for an event

Not super exciting, but it worked.

Avoiding Python

After some searching, I figured out a couple of options to display text on the display.

  • Use epaper.js, a command line application that can render a URL onto an epaper display
  • Use Waveshare’s Python demo as a starting point and add on some custom code

graph LR; A{gcalendar} --> B(Generate HTML) B --> C(Serve HTML) C --> D(Display with epaper.js) A --> E(Learn some Python) E --> F(Insert events into Python demo)
This was just an excuse to try out Hugo's mermaid diagrams

I decided I didn’t want to learn Python at the moment and went with the epaper.js route. I followed the installation instructions in the epaper.js repo. Then I created a super simple html website using txti.es.

I ran the following command ejs refresh rpi-2in13-v2 "http://txti.es/qr8k1" et voilĂ : hello world! how original…

From these instructions I learned how to use a bash script to generate HTML from the gcalendar output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
#Generates an html page with upcoming schedule 

gcal=$(gcalendar --calendar WorkCal --no-of-days 1)

cat <<- _EOF_
    <html>
    <head>
        <title>
        Schedule
        </title>
    </head>

    <body>
    <h1>Up Next</h1>
    <p>$gcal</p>
    </body>
    </html>
_EOF_

I then served up the generated index.html locally with http-server. Then I ran the epaper.js command to display the page on my epaper display.
ejs refresh rpi-2in13-v2 "http://localhost:8080" calendar events on an epaper display

The font needs some work and I need to figure out line breaks

While it technically worked, it wasn’t exactly useful, and it seemed like way too many steps for mediocre results.

Embracing Python

I really avoided the Python route because I know next to nothing about programming, but it became clear it was probably the easier route to get my epaper calendar display working. After a little trial and error with the Waveshare Python demo, I was able to strip out all of the steps other than the step that displayed some text. I repositioned the text and was able to output a decent first step.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python
# -*- coding:utf-8 -*-
import os
picdir = 'pic'
libdir = 'lib'
from lib.waveshare_epd import epd2in13_V2
from PIL import Image,ImageDraw,ImageFont

try:  
    epd = epd2in13_V2.EPD()
    epd.init(epd.FULL_UPDATE)
    epd.Clear(0xFF)

    font24 = ImageFont.truetype(os.path.join(picdir, 'Font.ttc'), 24)
    
    image = Image.new('1', (epd.height, epd.width), 255)  # 255: clear the frame    
    draw = ImageDraw.Draw(image)
    
    draw.text((0, 0), 'Time goes here', font = font24, fill = 0)
    draw.text((0, 25), 'Event title goes here', font = font24, fill = 0)
    epd.display(epd.getbuffer(image))
    epd.sleep()
    
except KeyboardInterrupt:    
    epd2in13_V2.epdconfig.module_exit()
    exit()

test text: time goes here, event title goes here

A lot more googling led me to Python’s subprocess so I could run the gcalendar command and capture in the output. I then replaced my test text with the gcalendar output. list of events overflowing the edge of the display

Baby steps

Clearly, the output formatting was suboptimal. I wanted to drop the date, use a 12 hour clock, and put the event title on a second line. A plain text output wasn’t going to work, so I’d need to switch to gcalendar’s json output.

[{"calendar_color": "#143648", "summary": "Test event 1", "start_date": "2022-03-13", "start_time": "09:00", "end_date": "2022-03-13", "end_time": "10:00", "location": ""}, {"calendar_color": "#143648", "summary": "Test event 2", "start_date": "2022-03-13", "start_time": "12:00", "end_date": "2022-03-13", "end_time": "13:00", "location": ""}, {"calendar_color": "#143648", "summary": "Test event 3 with a long title to test out splitting this on two lines", "start_date": "2022-03-13", "start_time": "15:00", "end_date": "2022-03-13", "end_time": "16:00", "location": ""}]
gcalendar json output

Many google searches later I figured out how to load the json as Python list of dictionaries and pick out individual values (e.g. only the start time, end time, and title of the first event).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import json
import subprocess
gcal = subprocess.run(["gcalendar", "--calendar", "WorkCal", "--no-of-days", "1", "--output", "json"], capture_output=True)
gcal_json = gcal.stdout.decode("utf-8")
gcal_python = json.loads(gcal_json)

time0 = gcal_python[0]['start_time'] + ' - ' + gcal_python[0]['end_time']
title0 = gcal_python[0]['summary']

print(time0)
print(title0)

09:00 - 10:00
Test event 1

Thanks to a Stack Overflow thread, I was also able to convert from 24 time to 12 hour time with AM/PM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import json
import subprocess
from datetime import datetime
gcal = subprocess.run(["gcalendar", "--calendar", "WorkCal", "--no-of-days", "1", "--output", "json"], capture_output=True)
gcal_json = gcal.stdout.decode("utf-8")
gcal_python = json.loads(gcal_json)

start0 = datetime.strptime(gcal_python[0]['start_time'], "%H:%M")
end0 = datetime.strptime(gcal_python[0]['end_time'], "%H:%M")
time0 = start0.strftime("%I:%M %p") + ' - ' + end0.strftime("%I:%M %p")

title0 = gcal_python[0]['summary']

print(time0)
print(title0)

09:00 AM - 10:00 AM
Test event 1

Finally, I wrapped the text for long event titles (again thanks to Stack Overflow).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json
import subprocess
from datetime import datetime
from textwrap import wrap
gcal = subprocess.run(["gcalendar", "--calendar", "WorkCal", "--no-of-days", "1", "--output", "json"], capture_output=True)
gcal_json = gcal.stdout.decode("utf-8")
gcal_python = json.loads(gcal_json)

start1 = datetime.strptime(gcal_python[1]['start_time'], "%H:%M")
end1 = datetime.strptime(gcal_python[1]['end_time'], "%H:%M")
time1 = start1.strftime("%I:%M %p") + ' - ' + end1.strftime("%I:%M %p")

title1 = gcal_python[1]['summary']

print(time1)
if len(title1) > 25:
    title1_split = wrap(gcal_python[1]['summary'], 25)
    print(title1_split[0])
    print(title1_split[1])
else:
    print(title1)
print('FULL LONG TITLE:')
print(title1)

03:00 PM - 04:00 PM
Test event 3 with a long
title to test out
FULL LONG TITLE:
Test event 3 with a long title to test out splitting this on two lines

Putting this together with the Waveshare demo, I was successfully able to display the next two events on my Google Calendar on the epaper display.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/python
# -*- coding:utf-8 -*-
import os
import json
import subprocess
from datetime import datetime
from textwrap import wrap
from lib.waveshare_epd import epd2in13_V2
from PIL import Image,ImageDraw,ImageFont
picdir = 'pic'
libdir = 'lib'

gcal = subprocess.run(["gcalendar", "--calendar", "WorkCal", "--no-of-days", "1", "--output", "json"], capture_output=True)
gcal_json = gcal.stdout.decode("utf-8")
gcal_python = json.loads(gcal_json)

start0 = datetime.strptime(gcal_python[0]['start_time'], "%H:%M")
end0 = datetime.strptime(gcal_python[0]['end_time'], "%H:%M")
time0 = start0.strftime("%I:%M %p") + ' - ' + end0.strftime("%I:%M %p")
start1 = datetime.strptime(gcal_python[1]['start_time'], "%H:%M")
end1 = datetime.strptime(gcal_python[1]['end_time'], "%H:%M")
time1 = start1.strftime("%I:%M %p") + ' - ' + end1.strftime("%I:%M %p")

title0 = gcal_python[0]['summary']
title1 = gcal_python[1]['summary']

try:  
    epd = epd2in13_V2.EPD()
    epd.init(epd.FULL_UPDATE)
    epd.Clear(0xFF)

    font22 = ImageFont.truetype(os.path.join(picdir, 'Font.ttc'), 21)

    image = Image.new('1', (epd.height, epd.width), 255)  # 255: clear the frame    
    draw = ImageDraw.Draw(image)
    
    draw.text((0, 0), '%s' % time0, font = font22, fill = 0)
    if len(title0) > 25:
        title0_split = wrap(gcal_python[0]['summary'], 25)
        draw.text((5, 20), '%s' % title0_split[0], font = font22, fill = 0)
        draw.text((5, 40), '%s' % title0_split[1], font = font22, fill = 0)
    else:
        draw.text((5, 20), '%s' % title0, font = font22, fill = 0)
    
    draw.text((0, 60), '%s' % time1, font = font22, fill = 0)
    if len(title1) > 25:
        title1_split = wrap(gcal_python[1]['summary'], 25)
        draw.text((5, 80), '%s' % title1_split[0], font = font22, fill = 0)
        draw.text((5, 100), '%s' % title1_split[1], font = font22, fill = 0)
    else:
        draw.text((5, 80), '%s' % title1, font = font22, fill = 0)
    
    epd.display(epd.getbuffer(image))
    epd.sleep()    
except KeyboardInterrupt:    
    epd2in13_V2.epdconfig.module_exit()
    exit()

the finished product: epaper display with two calendar events
I'll admit, I was pretty proud of this one

Automate with crontab

The last thing I needed to do was run my python script at regular intervals. I added the python script to the pi user’s crontab and set it to run every 15 minutes. I also had to add a PATH definition to the crontab so gcalendar ran properly (thanks Stack Overflow yet again).

Improvements

It’s working, but my upcoming meeting display isn’t perfect. In the future I’d like to make some quality of life changes, but that will have to wait for a future post.

To Do

  1. Fix error when there is less than two future meetings
  2. Add a message when there are no meetings
  3. Figure out how to deal with overlapping meetings
  4. Flip the screen so the power port is on the top
  5. Hide the current meeting when it’s almost over


See also