A nerd friend bought me a LILYGO E-paper display1, in the exact hope that I’d end up going down the rabbit hole of Smart Things. I have gone down this bloody rabbit hole. I’ve always been a sucker for data, and I aspire to heating my home more efficiently by measuring temperatures round the house and seeing how they change as I change demand.
This is not a place of honour. It’s a place of a lot of YAML and black magic. It’s pretty cool when it works though:
- The board is a micro with Wi-Fi and a big-enough E-Paper Display (EPD)
- I can reconfigure it via context-aware text editor in the browser
- Push updates (also from the browser)
- Do some reasonably complex data processing and image/text rastering on the micro itself
- Control it from either a self-hosted server on the micro or from Home Assistant
Details
I’ve done things in Docker for the moment, although they may not end up staying there. My Synology NAS can run containers pretty painlessly, and understands Docker Compose2. The relevant parts of the config file is at the bottom of this post. Due to both services using mDNS for their autodiscovery, host networking is required.
On starting Home Assistant and browsing to it, it’ll ask a bunch of obvious questions and set itself up. Home Assistant and ESPhome will then auto-discover each other.
To actually program the Lilygo for the first time, you need to have it connected via USB.
Chrome can actually do this if you set up TLS for the dashboard, but I found it simpler to to all the configuration and setup in dash, and then you can use the Manual Download option and I ran esptool myself on my desktop (pip3 install --user esptool
).
Don’t use the ’legacy’ file.
esptool.py --chip esp32 -p /dev/ttyACM0 write_flash 0x0 lilygo-factory.bin
After this it won’t need a physical connection again.
The configuration file I’m currently using is shown below, and I will change it a lot I think, but I needed to start somewhere and I’m happy with this as something usable for a bit. It features:
- Not one but TWO sizes of fonts
- Icons to look pretty and clean
- Current temperature
- Daily high and low temperature
- Graph of the temperature over the last 6h
- Current exchange rate between the UK and Canada
- Sunrise/sunset time
- Text string controllable from HA
- Buttons are visible in HA (not sure if this is useful, mind)
The sunrise/sunset one is a bit of a pain. Technically I think the simplest thing is to let ESPhome calculate this in its own Sun component, however as there’s one set up in HA and I don’t want to hardcode destinations everywhere3.
TODO
This isn’t “perfect” yet, but I haven’t worked out exactly what it needs to do. I have some vague ideas:
- Go to sleep and save some power
- Get prettier, like in this fancy config
- Slap a battery in it (ordered one!)
- Print a case!
- Stop the EPD fading out when losing power4, maybe by calibrating
- Maybe try a new way of controlling the display, keeping an eye on this bug
- Log to MQTT, but I’m a bit worried this might confuse HA
- Have some pages and cycle through them?
Docker compose snippet
The config here is starting Home Assistant and ESPhome. I originally had HA depend on ESPhome but I realised that while I’d prefer HA to come up last it doesn’t actually require it and certainly shouldn’t be prevented from starting.
I don’t really like using host mode networking but it’s needed by default for mDNS and while apparently one can work round that, I haven’t tried it yet.
version: '3'
homeassistant:
container_name: homeassistant
image: "ghcr.io/home-assistant/home-assistant:stable"
volumes:
- /volume1/docker/homeassistant:/config
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
privileged: true # for when we have USB
network_mode: host # for mDNS
depends_on:
- esphome
esphome:
container_name: esphome
image: ghcr.io/esphome/esphome
volumes:
- /volume1/docker/esphome:/config
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
network_mode: host # required for mDNS
environment:
- USERNAME=xxx
- PASSWORD=xxx
healthcheck:
test: "curl -f http://localhost:6052/version -A HealthCheck || exit 1"
interval: 30s
timeout: 30s
Final configuration
This requires entries in the secrets.yaml
for all the !secret
directives.
substitutions:
esp_name: lilygo
font_small: "48"
font_big: "100"
esphome:
name: ${esp_name}
comment: "E-paper display"
friendly_name: lilygo
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
# To connect *to* HA *from* lilygo
api:
encryption:
key: !secret ha_key
ota:
password: !secret lilygo_ota
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive_portal node) in case wifi connection fails
ap:
ssid: "Lilygo Fallback Hotspot"
password: !secret fallback
captive_portal:
external_components:
- source: github://tiaanv/esphome-components
components:
- "t547"
time:
- platform: homeassistant
id: ntp
timezone: "America/Toronto" # Picking up the timezone from HA would be nice
# Setting these buttons with names auto-exposes them to HA
button:
- platform: restart
name: "${esp_name} Restart"
- platform: template
name: "${esp_name} Refresh"
icon: "mdi:update"
on_press:
then:
- component.update: t5_disp
# Fonts are downloaded from Google at build time, which is pretty neat.
# Get codepoint values from the Google Fonts website.
font:
- file: "gfonts://Roboto"
id: roboto
size: $font_small
- file:
type: gfonts
family: Roboto
weight: 500
id: din_big
# Glyphs are prerastered, choose them early to save flash
glyphs: " C$+-0123456789.:"
size: $font_big
- file: 'gfonts://Material+Symbols+Outlined'
id: font_icons_small
size: $font_small
glyphs: # also as a list
- "\U0000E1C6" # Sunrise
- "\U0000EF44" # Sunset
- "\U0000F582" # Temperature High
- "\U0000F581" # Temperature Low
- file: 'gfonts://Material+Symbols+Outlined'
id: font_icons_big
size: $font_big
glyphs:
- "\ue1ff" # device_thermostat
- "\ueb70" # currency exchange
binary_sensor:
- platform: status
name: "status"
# Setting up the buttons here to refresh the display
# (for experimenting with sleep mode) and to flip between
# pages on the display. Only one is defined at the bottom
# right now
- platform: gpio
pin:
number: GPIO39
inverted: true
internal: true
on_press:
then:
- component.update: t5_disp
name: "Button 1"
- platform: gpio
pin:
number: GPIO34
inverted: true
name: "Button 2"
internal: true
on_press:
then:
- display.page.show_previous: t5_disp
- component.update: t5_disp
- platform: gpio
pin:
number: GPIO35
inverted: true
name: "Button 3"
internal: true
on_press:
then:
- display.page.show_next: t5_disp
- component.update: t5_disp
# Import some strings from HA - annoyingly we can't import
# times from HA *as times* and strftime them, so these need
# a template/helper on HA
text_sensor:
- platform: homeassistant
entity_id: sensor.next_rising_time
id: sunrise
- platform: homeassistant
entity_id: sensor.next_setting_time
id: sunset
# This bit basically defines a textbox that HA can fill
# called 'alert' that the micro won't update itself (because
# nothing will) but will prompt a redraw of the screen
text:
mode: text
name: alert
id: alert
platform: template
optimistic: True
# maybe add a startup set of this state so HA doesn't say Unknown
update_interval: never
on_value:
then:
- component.update: t5_disp
sensor:
- platform: homeassistant
name: "Current temperature"
entity_id: sensor.gatineau_temperature
id: temp
- platform: homeassistant
name: "Daily high"
entity_id: sensor.gatineau_high_temperature
id: daily_high
- platform: homeassistant
name: "Daily low"
entity_id: sensor.gatineau_low_temperature
id: daily_low
- platform: homeassistant
name: "GBP CAD"
entity_id: sensor.gbp_cad
id: gbp_cad
on_value: # Actions to perform once data for the last sensor has been received
then:
- script.execute: all_data_received
script:
- id: all_data_received
then:
- component.update: t5_disp
graph:
# Show bare-minimum auto-ranged graph. It is not saved so
# any power loss or restart loses it. It'll take a while to fill
- id: temp_graph
sensor: temp
duration: 6h
x_grid: 1h
y_grid: 5.0 # degC/div
width: 400
height: 400
display:
- platform: t547
id: t5_disp
rotation: 90
update_interval: 5min
pages:
- id: weather
lambda: |-
it.print( 40, 60, id(font_icons_big), TextAlign::CENTER_LEFT, "\ue1ff");
it.printf(130, 60, id(din_big), TextAlign::CENTER_LEFT, "%3.1fC", id(temp).state);
it.graph((it.get_width() / 2)-200, 140, id(temp_graph)); // no support for ImageAlign in graph()
it.print( 70, 600, id(font_icons_small), TextAlign::CENTER_LEFT, "\U0000F582");
it.printf(260, 600, id(roboto), TextAlign::CENTER_RIGHT, "%.1fC", id(daily_high).state);
it.print( 70, 660, id(font_icons_small), TextAlign::CENTER_LEFT, "\U0000F581");
it.printf(260, 660, id(roboto), TextAlign::CENTER_RIGHT, "%.1fC", id(daily_low).state);
it.print( 270, 600, id(font_icons_small), TextAlign::CENTER_LEFT, "\uE1C6");
it.print( 340, 600, id(roboto), TextAlign::CENTER_LEFT, id(sunrise).state.c_str());
it.print( 270, 660, id(font_icons_small), TextAlign::CENTER_LEFT, "\uEF44");
it.print( 340, 660, id(roboto), TextAlign::CENTER_LEFT, id(sunset).state.c_str());
it.printf(130, 800, id(din_big), TextAlign::CENTER_LEFT, "C$ %.2f", id(gbp_cad).state);
it.print( 20, 880, id(roboto), TextAlign::TOP_LEFT, id(alert).state.c_str());
# This prints a thermometer and the current temperature
# Then a temperature graph
# Then the daily high and low temperatures to the left
# and sunrise/sunset to the right
# Next is the GBP/CAD exchange rate
# Finally that arbitrary string
Actually they don’t make it any more, it’s, uh, sat on my shelf a bit. Now there’s this one but I’m not sure what the esphome support is like. ↩︎
It is just an expensive Linux box after all. You’d hope. ↩︎
Although maybe a template in ESPhome might help here. ↩︎
This might be a better EPD component but as the author made an esphome fork rather than a component library I think need to check it out differently. ↩︎