e-paper price tag next calendar event
Here I'm documenting the steps (in varying level of detail) for posterity and others to get an e-paper price tag with the next event of my day merging multiple calendars together.
This is what the end result looks like (that's a 2.9” epaper display and looks so much nicer in-person) :
So what do you need to make that happen?
- Calendars in Home Assistant
- OpenEPaperLink Access Point
- 2.9” tag with OpenEPaperLink firmware
- Some fun with jinja templating
Calendars in Home Assistant
Google Calendar
Where I work uses Google Calendar and have not disabled the iCal feed. Where you work may use something different, and also you might want to check with someone if it's against policy to have calendar entries exported.
To get that Google Calendar into Home Assistant is relatively simple, you add the ICS Calendar integration, give it the “Secret address in iCal format” from the settings of the calendar, name it something like “Work” and done.
NextCloud Calendar
I use NextCloud for my personal calendar so I don't miss out on things like game night.
Again, simple integration, this time the CalDAV one. Go into the Calendar app on NextCloud, click settings and scroll down until you get to “Copy primary CalDAV address”. Enter that, along with credentials into HomeAssistant, and calendar is set up.
Others
There are integrations for other calendars but I don't use them, so YMMV, but worth a shot.
OpenEPaperLink Access Point
Okay, this takes a little explaining. OpenEPaperLink (henceforth OEPL) is a combination of custom firmware for various price tags/ePaper displays and an Access Point for sending data to them.
Generally, these use the same radio (and therefore channels in the 2.4Ghz spectrum) as ZigBee (802.15.4) but without the ZigBee protocol layer. Yes, that means your existing ZigBee setup won't help you here.
Note: I'm ignoring the rarer price tag types, which need another radio (known as subghz) and bluetooth-only tags which to my understanding are currently problematic for the AP.
To make things easy, OEPL has created multiple types of access point. Usually this is an ESP32-S3 coupled with an ESP32-C6. You need both pieces to make an AP because you need to be talking wifi (s3) and 802.15.4 (c6) at the same time. I'm actually using a much older AP based on the ESP32-S2 and a spare segmented tag which is acting in place of the ESP32-C6, but that's not getting any updates now.
Choices are here : – The easiest (but definitely not the cheapest) AP is the LilyGo T-Panel AP. – If you're not so good at soldering, and want a cheap option you can use dupont cables and make the Spaghetti AP. – If you want a cheap but nice option and don't mind ordering PCBs and a bit of soldering, the Yellow AP is probably for you.
Bear in mind this blog post was written in February 2025, so you might want to browse the OEPL github wiki to see if things have changed here.
Once you have an AP, connect it to your network and it will talk to any flashed tags you have.
E-paper price tag
I'm using the most common tag, manufactured by Solum, the ST‐GR29000. There are a lot of tags that have OpenEPaperLink (OEPL) firmware, and getting them is sometimes hard. eBay sometimes has them, aliexpress rarely, and the discord (check the homepage for a current link) that's used for OEPL has some trading because when people find a tag they like, they often buy in bulk, and have spares.
The tags come in different shapes and sizes, the code on this page is for a 2.9” tag at a resolution of 296x128px. It should work for any tag that is the same resolution, or you might want to tweak the positions of things if you have a different resolution.
Also fun to know, ePaper usually comes in Black and White; Black, White and Red or Black White and Yellow. I like BWR myself, and if you only have a Black and White tag, you might need to change the Red to Black in the code, not sure what the integration would do if you sent red to a Black&White tag. I don't know if OEPL supports more colours (the integration doesn't) as colour ePaper screens are still quite rare.
Mine came pre-flashed with the OEPL firmware because they were used by someone else before. If you have to flash more than one, you probably want to make a jig for it.
Fun with templates
Once you have all the above, install the OpenEPaperLink (OEPL) integration. You can get this via HACS, which I recommend, and doesn't require your HA be connected to the “My Home Assistant” Cloud stuff.
Home Assistant uses the Jinja templating language to template text. The OEPL integration provides a new action called DrawCustom that you can call from an automation and allows you to describe in YAML form what you want to draw. That's a choice (one that the HA ecosystem leaves you with little choice in) but we work with what we have.
One of the people on the aforementioned discord (check the homepage for a current link) posted an agenda view automation and combined their calendars, and I iterated on that to provide this one.
Here's the juicy YAML:
alias: Next Event
description: ""
triggers:
- hours: /1
trigger: time_pattern
minutes: /5
conditions:
- condition: time
after: "05:00:00"
before: "23:00:00"
actions:
- alias: Get events 0 day(s) from now (filtered out past events)
data:
start_date_time: "{{ now() }}"
end_date_time: "{{today_at('00:00') + timedelta(days=1)}}"
response_variable: cal_day_0
target:
entity_id:
- calendar.personal
- calendar.work
action: calendar.get_events
- alias: Get events 1 day(s) from now
data:
start_date_time: "{{ today_at('00:00') + timedelta(days=1) }}"
end_date_time: "{{ today_at('00:00') + timedelta(days=2) }}"
response_variable: cal_day_1
target:
entity_id:
- calendar.personal
- calendar.work
action: calendar.get_events
- alias: Get events 2 day(s) from now
data:
start_date_time: "{{ today_at('00:00') + timedelta(days=2) }}"
end_date_time: "{{ today_at('00:00') + timedelta(days=3) }}"
response_variable: cal_day_2
target:
entity_id:
- calendar.personal
- calendar.work
action: calendar.get_events
- alias: Get events 3 day(s) from now
data:
start_date_time: "{{ today_at('00:00') + timedelta(days=3) }}"
end_date_time: "{{ today_at('00:00') + timedelta(days=4) }}"
response_variable: cal_day_3
target:
entity_id:
- calendar.personal
- calendar.work
action: calendar.get_events
- alias: Extract events from all calendars
variables:
header: Next Event
timeheaderstart: Start
timeheaderstartx: 266
timeheaderend: End
timeheaderendx: 272
events_day0: >
{{ cal_day_0.values() | map(attribute='events') | list |
sum(start=[])}}
events_day1: >
{{ cal_day_1.values() | map(attribute='events') | list |
sum(start=[])}}
events_day2: >
{{ cal_day_2.values() | map(attribute='events') | list |
sum(start=[])}}
events_day3: >
{{ cal_day_3.values() | map(attribute='events') | list |
sum(start=[])}}
days:
- Today
- Tomorrow
- Ubermorgen
- 3 Days away
- alias: Send generated Event to OEPL
metadata: {}
data:
rotate: 0
dry-run: false
background: white
payload:
- type: text
value: |
{{ header}}
font: ../../media/OpenSans-Bold.ttf
x: 10
"y": 10
size: 20
color: black
- type: text
value: |
{% if events_day0 | count > 0 %}
{{ days[0] }}
{% elif events_day1 | count > 0 %}
{{ days[1] }}
{% elif events_day2 | count > 0 %}
{{ days[2] }}
{% else %}
{{ days[3] }}
{% endif %}
font: ../../media/OpenSans-Bold.ttf
x: 10
"y": 50
size: 28
color: red
- type: text
value: |
{%- set events = [{}] %}
{% if events_day0 | count > 0 %}
{%- set events = events_day0 | sort(attribute='start') | list %}
{% elif events_day1 | count > 0 %}
{%- set events = events_day1 | sort(attribute='start') | list %}
{% elif events_day2 | count > 0 %}
{%- set events = events_day2 | sort(attribute='start') | list %}
{% elif events_day3 | count > 0 %}
{%- set events = events_day3 | sort(attribute='start') | list %}
{% else %}
{%- set events = [{}] %}
{% endif %}
{%- set event = events[0] %}
{%- if 'start' in event -%}
{% if as_timestamp(event.start) | timestamp_custom('%H:%M') == "00:00"%}
All day: {{ event.summary }}
{% else %}
{{ event.summary }}
{% endif %}
{%- endif -%}
font: ../../media/OpenSans-Bold.ttf
"y": 105
size: 14
x: 10
- type: text
value: |
{{ timeheaderstart }}
font: ../../media/OpenSans-Bold.ttf
x: |
{{ timeheaderstartx }}
"y": 5
size: 11
color: red
- type: text
value: |
{{ timeheaderend }}
font: ../../media/OpenSans-Bold.ttf
x: |
{{ timeheaderendx}}
"y": 55
size: 11
color: red
- type: multiline
delimiter: §
font: ../../media/OpenSans-Bold.ttf
value: |
{%- set events = [{}] %}
{% if events_day0 | count > 0 %}
{%- set events = events_day0 | sort(attribute='start') | list %}
{% elif events_day1 | count > 0 %}
{%- set events = events_day1 | sort(attribute='start') | list %}
{% elif events_day2 | count > 0 %}
{%- set events = events_day2 | sort(attribute='start') | list %}
{% elif events_day3 | count > 0 %}
{%- set events = events_day3 | sort(attribute='start') | list %}
{% else %}
{%- set events = [{}] %}
{% endif %}
{%- set event = events[0] %}
{%- if 'start' in event -%}
{{- as_timestamp(event.start) | timestamp_custom('%H:%M') }}§{{as_timestamp(event.end) | timestamp_custom('%H:%M') }}
{% endif -%}
start_y: 32
x: 190
size: 40
offset_y: 50
color: black
action: open_epaper_link.drawcustom
target:
device_id: b173ed2bf21ca8444c86afb61ef222e1
mode: single
You'll need to grab the font and put it in your Home Assistant media
directory (mine is /config/media
but your setup might be different).
I've made some config variable in Extract events from all calendars
like the heading, and words for Today/Tomorrow/Übermorgen/In 3 days (I love the word Übermorgen), and you can left-align Start/End to the time by setting their x values in there to 190
if you prefer that look.
Comments on this blog post can be sent direct on the fediverse (any activitypub server like mastodon etc.) on @martyn@toot.martyn.berlin
.