ESPHome Oracle Code Card ePaper Display
October 20, 2020I'm a big fan of ePaper displays - it's easier on the eyes compared to LED based displays and is great for low powered devices like Pebble Watches and Kindle eBook readers.
I'm using an Oracle Code Card badge I got at a conference. It has a 2.7 inch display and is based on the $29 badgy if you want to do this project yourself.
Finished Product:
The Setup:
You need to add a couple sensors to home assistant first. You can find the specific file here in @fredrike's github page. Add this file to your packages folder inside your config folder. (here: ~/config/packages
). If you don't already have a packages folder, make sure to include this line under the homeassistant:
header like so:
homeassistant:
packages: !include_dir_named packages
Home Assistant Configuration:
the blank " " squares you see in the config are Material Design Icons. Sometimes the icons get lost in translation when copy/pasting them.
If you're unable to copy/paste the code below and retain the Material Design Icons, try copying/pasting from here:
https://github.com/fredrike/esphome-nodes/blob/master/homeassistant-config/package-display_node.yaml
If that doesn't work, most of them are commented, so you can copy/paste each individual one from here: Material Design Icons.
~/config/packages/display_node.yaml
sensor:
- platform: moon
- platform: template
sensors:
moon_tpl:
entity_id: sensor.moon
value_template: >-
{% set map = {
"new_moon": "",
"waxing_crecent": "",
"first_quarter": "",
"waxing_gibbous": "",
"full_moon": "",
"waning_gibbous": "",
"last_quarter": "",
"waning_crescent": ""
} %}
{{- map[states('sensor.moon')] -}}
- platform: template
sensors:
forecast_today:
entity_id: sensor.time
value_template: >-
{% set weather = {
"clear-day": "",
"clear-night": "",
"cloudy": "",
"rain": "",
"sleet": "",
"snow": "",
"wind": "",
"fog": "",
"partly-cloudy-day": "",
"partly-cloudy-night": ""
} %}
{% set base = 'sensor.dark_sky_forecast_' %}
{{- weather[states('{}icon_{}d'.format(base, 0))] -}};
{{- states('{}apparent_temperature'.format(base)) -}}°;
{{- states('{}precip_probability_{}d'.format(base, 0))|int -}}
- platform: darksky #https://pastebin.com/iX7u4ZSg
api_key: !secret darksky_api_key
name: dark_sky_forecast
scan_interval: '00:10'
forecast:
- 0
- 1
- 2
- 3
- 4
monitored_conditions:
- icon
- temperature
- apparent_temperature
- temperature_high
- temperature_low
- precip_probability
- precip_intensity
- platform: template
sensors:
forecast:
entity_id: sensor.time
value_template: >-
{% set dow = {
0: "Mon",
1: "Tue",
2: "Wed",
3: "Thu",
4: "Fri",
5: "Sat",
6: "Sun"
} %}
{% set weather = {
"clear-day": "mdi:weather-sunny",
"clear-night": "mdi:weather-night",
"cloudy": "mdi:weather-cloudy",
"rain": "mdi:weather-pouring",
"sleet": "mdi:weather-snowy-rainy",
"snow": "mdi:weather-snowy",
"wind": "mdi:weather-windy",
"fog": "mdi:weather-fog",
"partly-cloudy-day": "mdi:weather-partly-cloudy",
"partly-cloudy-night": "mdi:weather-night-partly-cloudy"
} %}
{% set weather = {
"clear-day": "",
"clear-night": "",
"cloudy": "",
"rain": "",
"sleet": "",
"snow": "",
"wind": "",
"fog": "",
"partly-cloudy-day": "",
"partly-cloudy-night": ""
} %}
{% set base = 'sensor.dark_sky_forecast_' %}
{%- for i in [ 1, 2, 3, 4] -%}
{{ dow[(now().weekday() + i) % 7] }};
{{- weather[states('{}icon_{}d'.format(base, i))] -}};
{{- states('{}daytime_high_temperature_{}d'.format(base, i))|int
-}}°/
{{- states('{}overnight_low_temperature_{}d'.format(base, i))|int -}}°;
{{- states('{}precip_probability_{}d'.format(base, i))|int -}}%
{%- if not loop.last %};{% endif %}
{%- endfor %}
ESPHome Code:
If you're unable to copy/paste the code below and retain the Material Design Icons, try copying/pasting from here:
https://gist.github.com/michaellunzer/37f29462ee9a17f22978e672c59e440c
esphome:
name: codecard3
platform: ESP8266
board: esp12e
# on_boot:
# - output.turn_off: onboard_led
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
spi:
clk_pin: GPIO14
mosi_pin: GPIO13
# s= '@!"%()+,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz.'
# print([','.join(_) for _ in s])
font:
- file: 'fonts/Google_Sans_Bold.ttf'
id: clock_font
size: 45
glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z','å', 'ä', 'ö', '/']
- file: 'fonts/Google_Sans_Medium.ttf'
id: temp_font
size: 32
glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z','å', 'ä', 'ö', '/']
- file: 'fonts/Google_Sans_Bold.ttf'
id: status_font
size: 18
glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'å', 'ä', 'ö', '/']
- file: 'fonts/Google_Sans_Medium.ttf'
id: aqi_font
size: 18
glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'å', 'ä', 'ö', '/']
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font
size: 32
glyphs: [
'', # mdi-account-heart
'', # mdi-thermometer
'', # mdi-fire-hydrant
"", # mdi-umbrella
"", # mdi-home-thermometer-outline
"", # mdi-home-thermometer
"", # mdi-air-filter
# Wifi
'', # mdi-wifi-strength-outline
'', # mdi-wifi-strength-1
'', # mdi-wifi-strength-2
'', # mdi-wifi-strength-3
'', # mdi-wifi-strength-4
# Weather
"", # mdi-weather-sunny
"", # mdi-weather-night
"", # mdi-weather-cloudy
"", # mdi-weather-pouring
"", # mdi-weather-snowy-rainy
"", # mdi-weather-snowy-heavy
"", # mdi-weather-windy-variant
"", # mdi-weather-fog
"", # mdi-weather-night-partly-cloudy
"", # mdi-weather-partly-cloudy
# Moon
"", # new_moon
"", # waxing_crecent
"", # first_quarter
"", # waxing_gibbous
"", # full_moon
"", # waning_gibbous
"", # last_quarter
"", # waning_crescent
# Sun
"", # mdi-weather-sunset-down
"", # mdi-weather-sunset-up
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: weather_font
size: 50
glyphs: [
'', # mdi-account-heart
'', # mdi-thermometer
'', # mdi-fire-hydrant
"", # mdi-umbrella
"", # mdi-home-thermometer-outline
"", # mdi-home-thermometer
"", # mdi-air-filter
# Wifi
'', # mdi-wifi-strength-outline
'', # mdi-wifi-strength-1
'', # mdi-wifi-strength-2
'', # mdi-wifi-strength-3
'', # mdi-wifi-strength-4
# Weather
"", # mdi-weather-sunny
"", # mdi-weather-night
"", # mdi-weather-cloudy
"", # mdi-weather-pouring
"", # mdi-weather-snowy-rainy
"", # mdi-weather-snowy-heavy
"", # mdi-weather-windy-variant
"", # mdi-weather-fog
"", # mdi-weather-night-partly-cloudy
"", # mdi-weather-partly-cloudy
# Moon
"", # new_moon
"", # waxing_crecent
"", # first_quarter
"", # waxing_gibbous
"", # full_moon
"", # waning_gibbous
"", # last_quarter
"", # waning_crescent
# Sun
"", # mdi-weather-sunset-down
"", # mdi-weather-sunset-up
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: current_weather_font
size: 80
glyphs: [
'', # mdi-account-heart
'', # mdi-thermometer
'', # mdi-fire-hydrant
"", # mdi-umbrella
"", # mdi-home-thermometer-outline
"", # mdi-home-thermometer
"", # mdi-air-filter
# Wifi
'', # mdi-wifi-strength-outline
'', # mdi-wifi-strength-1
'', # mdi-wifi-strength-2
'', # mdi-wifi-strength-3
'', # mdi-wifi-strength-4
# Weather
"", # mdi-weather-sunny
"", # mdi-weather-night
"", # mdi-weather-cloudy
"", # mdi-weather-pouring
"", # mdi-weather-snowy-rainy
"", # mdi-weather-snowy-heavy
"", # mdi-weather-windy-variant
"", # mdi-weather-fog
"", # mdi-weather-night-partly-cloudy
"", # mdi-weather-partly-cloudy
# Moon
"", # new_moon
"", # waxing_crecent
"", # first_quarter
"", # waxing_gibbous
"", # full_moon
"", # waning_gibbous
"", # last_quarter
"", # waning_crescent
# Sun
"", # mdi-weather-sunset-down
"", # mdi-weather-sunset-up
]
# 264 x 176 display
display:
- platform: waveshare_epaper
id: epaper
cs_pin: GPIO2
busy_pin: GPIO5
reset_pin: GPIO4
dc_pin: GPIO0
model: 2.70in
rotation: 270°
# full_update_every: 30
# 264 x 176 display
update_interval: 3600s
lambda: |
int x, y;
ESP_LOGI("display", "Updating..");
/* Print time in HH:MM format */
it.strftime(0, -10, id(clock_font), TextAlign::TOP_LEFT, "%I:%M", id(current_time).now());
/* Print AM/PM */
it.strftime(120, 15, id(status_font), TextAlign::TOP_LEFT, "%P", id(current_time).now());
it.strftime(65, 40, id(status_font), TextAlign::TOP_LEFT, "%b %e, %Y", id(current_time).now());
// it.line(259, -10, 259, 200);it.line(261, 0, 261, 200);it.line(260, 0, 260, 200);
// it.line(0, 200, 259, 200);it.line(0, 201, 261, 201);it.line(0, 202, 260, 202);
/*
it.print(125, 120, id(temp_font), TextAlign::BASELINE_CENTER, "HELLO SUNSHINE!");
it.print(125, 160, id(icon_font), TextAlign::BASELINE_CENTER, "");
it.print(125, 182, id(temp_font), TextAlign::BASELINE_CENTER, "Lea & Emma"); // 400 - ((400 - 260)/2)
*/
/* Moon icon - used to be y = 155, x = 230 */
if(id(moon_icon).has_state()) {
y = 165, x = 20;
ESP_LOGI("Moon icon", "%s", id(moon_icon).state.c_str());
it.printf(x, y, id(icon_font), TextAlign::BASELINE_CENTER, "%s", id(moon_icon).state.c_str());
}
/* WiFi Signal Strenght x = 398, y = 298; */
if(id(wifisignal).has_state()) {
x = 175, y = 30;
if (id(wifisignal).state >= -50) {
//Excellent
it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "");
ESP_LOGI("WiFi", "Exellent");
} else if (id(wifisignal).state >= -60) {
//Good
it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "");
ESP_LOGI("WiFi", "Good");
} else if (id(wifisignal).state >= -67) {
//Fair
it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "");
ESP_LOGI("WiFi", "Fair");
} else if (id(wifisignal).state >= -70) {
//Weak
it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "");
ESP_LOGI("WiFi", "Weak");
} else {
//Unlikely working signal
it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "");
ESP_LOGI("WiFi", "Unlikely");
}
}
// Current weather
if(id(weather_forecast_today).has_state()) {
std::string str = id(weather_forecast_today).state;
ESP_LOGI("Weather", "%s", str.c_str());
std::size_t current, previous = 0;
char delim = ';';
current = str.find(delim);
int y = 100, x = 5;
for (int i=0; i<3; i++) {
if(i == 0) {
it.printf(x - 15, y, id(current_weather_font),
TextAlign::BASELINE_LEFT,
"%s", str.substr(previous, current - previous).c_str());
} else if (i == 1) {
ESP_LOGD("weather","%dx%d %s", (i % 1 ? 270 : 320), x,
str.substr(previous, current - previous).c_str());
/* it.printf(y, x, id(status_font),
TextAlign::TOP_LEFT,
"%s", str.substr(previous, current - previous).c_str());
*/
} else if (i == 2) { // Chance for rain
it.printf(x, y + 2, id(icon_font), TextAlign::TOP_LEFT, ""); // Umbrella
it.printf(x + 30, y + 10, id(status_font),
TextAlign::TOP_LEFT,
"%s%%", str.substr(previous, current - previous).c_str());
}
// y += 100;
previous = current + 1;
current = str.find(delim, previous);
}
}
// Print inside temperature (from homeassistant sensor)
y = 135, x = 100;
if (id(inside_temperature).has_state()) {
it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "");
it.printf(x, y + 4, id(temp_font), TextAlign::BOTTOM_LEFT , "%5.1f°", id(inside_temperature).state);
}
// Print outside temperature (from homeassistant sensor)
if (id(outside_temperature).has_state()) {
it.print(x, y + 4, id(icon_font), TextAlign::TOP_RIGHT, "");
it.printf(x, y, id(temp_font), TextAlign::TOP_LEFT, "%5.1f°", id(outside_temperature).state);
}
// Print sniffer0 air quality index sensor (from homeassistant sensor)
if (id(sniffer0_aqi).has_state()) {
x = 91, y = 60;
it.print(x + 10, y + 4, id(icon_font), TextAlign::TOP_RIGHT, "");
it.printf(x - 4, y, id(temp_font), TextAlign::TOP_LEFT, "%5.0f", id(sniffer0_aqi).state);
it.print(x + 87, y + 16, id(aqi_font), TextAlign::TOP_RIGHT, "AQI");
}
/*// Print consumer
if (id(top_consumer).has_state()) {
it.printf(0, 295, id(status_font), TextAlign::BOTTOM_LEFT , "Total: %.0f W, %s W", id(sparsnas).state, id(top_consumer).state.c_str());
}
*/
it.line(180, 0, 180, 176);
it.line(0, 103, 180, 103);
// std::string str = "mon:cloud:12.1:30%:tue:rain:10.2:40%:wed:sunny:18.0:0%:thu:snow:-12.2:10%";
// Mon;;65°/53°;12%;Tue;;70°/56°;7%;Wed;;73°/55°;2%;Thu;;73°/55°;1%
if(id(weather_forecast).has_state()) {
std::string str = id(weather_forecast).state;
ESP_LOGI("Weather", "%s", str.c_str());
std::size_t current, previous = 0;
char delim = ';';
current = str.find(delim);
int x = 240, y = 0;
// changed from x = 340, y = 4
for (int i=0; i<4; i++) {
ESP_LOGI("t", "%d", i);
for (int j=0; j<4; j++) {
if(j == 1) { // Weather icon.
it.printf(x - 65, y + 25, id(weather_font),
TextAlign::BASELINE_LEFT,
"%s", str.substr(previous, current - previous).c_str());
} else {
ESP_LOGD("weather","%dx%d %s", (j % 1 ? 270 : 320), x,
str.substr(previous, current - previous).c_str());
it.printf((j == 0 ? x - 15 : 390), y,
id(status_font),
(j == 0 ? TextAlign::TOP_LEFT : TextAlign::TOP_RIGHT),
"%s", str.substr(previous, current - previous).c_str());
y += 16;
}
previous = current + 1;
current = str.find(delim, previous);
}
y += 2;
}
}
# output:
# - platform: gpio
# id: onboard_led
# pin:
# number: 2
# inverted: False
# light:
# - platform: binary
# name: "Onboard LED"
# output: onboard_led
time:
- platform: homeassistant
id: current_time
timezone: America/Los_Angeles
on_time:
# Every 1 minutes
- seconds: 0
then:
- component.update: epaper
sensor:
- platform: homeassistant
id: inside_temperature
entity_id: sensor.sn1_temperature
internal: true
- platform: homeassistant
id: sniffer0_aqi
entity_id: sensor.sniffer0_pm_2_5_aqi
internal: true
- platform: homeassistant
id: outside_temperature
entity_id: sensor.openweathermap_forecast_temperature
internal: true
# - platform: homeassistant
# entity_id: sensor.sparsnas_energy_consumption_momentary
# id: sparsnas
# internal: true
- platform: homeassistant
entity_id: sensor.dark_sky_precip_probability
id: precip_probability
internal: true
- platform: wifi_signal
name: "WiFi Signal Sensor"
id: wifisignal
update_interval: 60s
text_sensor:
# - platform: homeassistant
# id: top_consumer
# entity_id: sensor.iotawatt_top_consumer
# internal: true
- platform: homeassistant
name: forecast
id: weather_forecast
entity_id: sensor.forecast
internal: true
- platform: homeassistant
name: forecast
id: weather_forecast_today
entity_id: sensor.forecast_today
internal: true
- platform: homeassistant
id: moon_icon
entity_id: sensor.moon_tpl
This was based heavily on @fredrike's code and so there are still some vestigal structures of his code commented out in my code. @Fredrike, if you see this, thanks for your inspiration!
Future hardware:
I realize that I'm pretty lucky to get ahold of a limited run piece of hardware specially made for a conference. I was hoping that I was watching YouTube while writing this post and saw a new product from Adafruit that this code could potentially work on in the future.
Also, the badgy looks like a great alternative.