In diesem Tutorial beschreibe ich wie ein Waveshare e-Paper Display über ESPHome mit Home Assistant verbunden wird und als Wetterstation und zum Anzeigen weiterer Sensor Daten verwendet werden kann.
Hardware
-
E-Paper Display: Waveshare 7.5 Inch E-Paper Display HAT Module V2 Kit 800×480 Resolution. Das Display führt einen SPI Anschluss über ein Flachbandkabel heraus und kann ohne Lötarbeiten mit dem unten genannten ESP32 Driver Board verbunden werden.
-
ESP32 Driver Board: Waveshare Universal e-Paper Driver Board mit Wifi und Bluetooth. Das Board erlaubt den direkten Anschluss des Display über SPI. Wichtig ist hier die ESP32 Version zu kaufen. Es gibt auch ein ESP8266 Driver Board das ich zuerst gekauft habe. Dieses hat aber für das 7,5″ E-Paper Display nicht genug Leistung.
-
Gehäuse: Waveshare 7.5inch e-Paper Raw Panel Case. Das Gehäuse ist perfekt auf das Display abgestimmt.
Es wird mit Magneten zusammengehalten, so das das Display sehr schnell ein- und ausgebaut werden kann. Ich habe den ESP32 einfach auf der Rückseite montiert und mit Holz und etwas Farbe einen Ständer dran gebaut. Siehe obige Bilder. Sehr Flache Kontroller finden ggf. auch im Gehäuse Platz.
Software Konfiguration
Datenquelle – Home Assistant Weather Integration
Als Datenquelle für die Wetterinformationen nutze ich die Home Assistant Weather Integration. Diese ist in der Regel standardmäßig in Home Assitant integriert. Dabei handelt es sich um folgende Kachel im UI:
Gespeist wird die Kachel über den Sensor „weather.home“ den du in den „Entwicklerwerkzeuge -> Zustände“ finden solltest:
Um die Wetterdaten an das Display zu senden habe ich zwei neue Template Sensoren angelegt die die Werte als CSV (Comma Separated Values) dem Display zu Verfügung stellen. Die Sensoren müssen in der „config/configuration.yaml“ Datei eingetragen werden.
sensor:
- platform: template
sensors:
epaper_weather_actual:
#weahtericon;temperature;humidity,pressure;wind_speed
value_template: "{{states('weather.home')}};{{state_attr('weather.home', 'temperature') | replace('.',',') }};{{state_attr('weather.home', 'humidity') | replace('.',',') }};{{state_attr('weather.home', 'pressure') | replace('.',',') }};{{state_attr('weather.home', 'wind_speed') | replace('.',',') }}"
epaper_weather_forecast:
value_template: "{{as_timestamp(state_attr('weather.home', 'forecast')[0]['datetime']) | timestamp_custom('%a') }};{{state_attr('weather.home', 'forecast')[0]['condition']}};{{state_attr('weather.home', 'forecast')[0]['temperature']| replace('.',',') }};{{state_attr('weather.home', 'forecast')[0]['templow']| replace('.',',') }};{{as_timestamp(state_attr('weather.home', 'forecast')[1]['datetime']) | timestamp_custom('%a') }};{{state_attr('weather.home', 'forecast')[1]['condition']}};{{state_attr('weather.home', 'forecast')[1]['temperature']| replace('.',',') }};{{state_attr('weather.home', 'forecast')[1]['templow']| replace('.',',') }};{{as_timestamp(state_attr('weather.home', 'forecast')[2]['datetime']) | timestamp_custom('%a') }};{{state_attr('weather.home', 'forecast')[2]['condition']}};{{state_attr('weather.home', 'forecast')[2]['temperature']| replace('.',',') }};{{state_attr('weather.home', 'forecast')[2]['templow']| replace('.',',') }};{{as_timestamp(state_attr('weather.home', 'forecast')[3]['datetime']) | timestamp_custom('%a') }};{{state_attr('weather.home', 'forecast')[3]['condition']}};{{state_attr('weather.home', 'forecast')[3]['temperature']| replace('.',',') }};{{state_attr('weather.home', 'forecast')[3]['templow']| replace('.',',') }};{{as_timestamp(state_attr('weather.home', 'forecast')[4]['datetime']) | timestamp_custom('%a') }};{{state_attr('weather.home', 'forecast')[4]['condition']}};{{state_attr('weather.home', 'forecast')[4]['temperature']| replace('.',',') }};{{state_attr('weather.home', 'forecast')[4]['templow']| replace('.',',') }}"
epaper_sunrise:
value_template: "{{ as_timestamp(state_attr('sun.sun', 'next_dawn')) | timestamp_custom('%H:%M') }}"
epaper_sunset:
value_template: "{{ as_timestamp(state_attr('sun.sun', 'next_dusk')) | timestamp_custom('%H:%M') }}"
ESP32 mit ESPHome einrichten
Das Display habe ich mit ESPHome eingerichtet. D.h. du must die ESPHome Integration in Home Assistant installieren und den ESP32 Mikrokontroller hinzufügen. Dazu verbindest du diesen mittels USB mit Home Assistant und gehst in die ESPHome Oberfläche:
Einstellungen -> Add-ons, Backups & Supervisor -> ESPHome -> Benutzeroberfläche öffnen
Hier kannst du dann mit mit „New Device“ den Mikrokontroller als ESP32 hinzufügen und diesem einen Namen geben. Siehe nachfolgende Screenshots:
Sobald der ESP32 in ESPHome integriert ist, sollte Home Assistant diesen als neue Integration finden. „Einstellungen -> Geräte & Dienste -> Integrationen“. Hier muss die Integration hinzugefügt werden, damit das Display auf die Wetterdaten von Home Assistant zugreifen kann.
ESPHome Display Konfiguration
Das Display nutzt Google Open Sans Font für die Schriften und Material Design Icon Font um z.B. die Wetter Bilder zu zeigen. Diese müsst ihr herunterladen und in den Order „config\esphome\fonts“ legen. Am einfachsten geht dies wenn ihr Samba auf dem Home Assistant einrichtet und dann über das Netzwerk auf die Verzeichnisse zugreift.
\\HOMEASSISTANT_IP\config\esphome\fonts
In der Konfiguration nutze ich die UniCodes der Bilder. Dies ist etwas robuster ein falsch eingestelltes Encoding der Konfigurationsdatei so keine Probleme bereitet. Die Unicodes der Material Design Icons kannst du hier finden.
Nachfolgend findest du meine ESPHome Konfiguration die eine Uhrzeit, Sonnen Auf- und Untergangzeiten, UpTime, WLAN Signalstärke und natürlich die Wetterdaten anzeigt:
esphome:
name: epaper75
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
password: "OTAPW"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Epaper75 Fallback Hotspot"
password: "FALLBACKPW"
captive_portal:
# Example configuration entry
spi:
clk_pin: 13
mosi_pin: 14
# --- Fonts --------------------------------------------------------------------
font:
- file: 'fonts/OpenSans-Bold.ttf'
id: openSansBold_font
size: 20
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/OpenSans-Bold.ttf'
id: watch_font
size: 90
glyphs:
[':', ".", '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
# https://pictogrammers.github.io/@mdi/font/6.5.95/ Unicodes
- file: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons_25
size: 25
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F0F55", # mdi-home-thermometer-outline
"\U000F0F54", # mdi-home-thermometer
"\U000F058E", # mdi-water-percent
"\U000F029A", # mdi-gauge
"\U000F059B", # mdi-weather-sunset
"\U000F059C", # mdi-weather-sunrise
"\U000F10C2", # mdi-thermometer-high
"\U000F10C3" # mdi-thermometer-low
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons_32
size: 32
glyphs: [
"\U000F091F", # mdi-wifi-strength-1
"\U000F0922", # mdi-wifi-strength-2
"\U000F0925", # mdi-wifi-strength-3
"\U000F0928", # mdi-wifi-strength-4
"\U000F092B", # mdi-wifi-strength-alert-outline
"\U000F0590", # weather-cloudy
"\U000F0F2F", # weather-cloudy-alert
"\U000F0E6E", # weather-cloudy-arrow-right
"\U000F0591", # weather-fog
"\U000F0592", # weather-hail
"\U000F0F30", # weather-hazy
"\U000F0898", # weather-hurricane
"\U000F0593", # weather-lightning
"\U000F067E", # weather-lightning-rainy
"\U000F0594", # weather-night
"\U000F0F31", # weather-night-partly-cloudy
"\U000F0595", # weather-partly-cloudy
"\U000F0F32", # weather-partly-lightning
"\U000F0F33", # weather-partly-rainy
"\U000F0F34", # weather-partly-snowy
"\U000F0F35", # weather-partly-snowy-rainy
"\U000F0596", # weather-pouring
"\U000F0597", # weather-rainy
"\U000F0598", # weather-snowy
"\U000F0F36", # weather-snowy-heavy
"\U000F067F", # weather-snowy-rainy
"\U000F0599", # weather-sunny
"\U000F0F37", # weather-sunny-alert
"\U000F14E4", # weather-sunny-off
"\U000F059A", # weather-sunset
"\U000F059B", # weather-sunset-down
"\U000F059C", # weather-sunset-up
"\U000F0F38", # weather-tornado
"\U000F059D", # weather-windy
"\U000F059E" # weather-windy-variant
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons_50
size: 80
glyphs: [
"\U000F0590", # weather-cloudy
"\U000F0F2F", # weather-cloudy-alert
"\U000F0E6E", # weather-cloudy-arrow-right
"\U000F0591", # weather-fog
"\U000F0592", # weather-hail
"\U000F0F30", # weather-hazy
"\U000F0898", # weather-hurricane
"\U000F0593", # weather-lightning
"\U000F067E", # weather-lightning-rainy
"\U000F0594", # weather-night
"\U000F0F31", # weather-night-partly-cloudy
"\U000F0595", # weather-partly-cloudy
"\U000F0F32", # weather-partly-lightning
"\U000F0F33", # weather-partly-rainy
"\U000F0F34", # weather-partly-snowy
"\U000F0F35", # weather-partly-snowy-rainy
"\U000F0596", # weather-pouring
"\U000F0597", # weather-rainy
"\U000F0598", # weather-snowy
"\U000F0F36", # weather-snowy-heavy
"\U000F067F", # weather-snowy-rainy
"\U000F0599", # weather-sunny
"\U000F0F37", # weather-sunny-alert
"\U000F14E4", # weather-sunny-off
"\U000F059A", # weather-sunset
"\U000F059B", # weather-sunset-down
"\U000F059C", # weather-sunset-up
"\U000F0F38", # weather-tornado
"\U000F059D", # weather-windy
"\U000F059E" # weather-windy-variant
]
# --- Display Layout -----------------------------------------------------------
display:
- platform: waveshare_epaper
cs_pin: 15
dc_pin: 27
busy_pin: 25
reset_pin: 26
model: 7.50inv2
update_interval: 60s
lambda: |
int x, y;
// Grid
it.line(0, 108, 800, 108); // Horizontal header
it.line(0, 440, 800, 440); // Horizontal footer
it.line(266, 108, 266, 440); // Vertical first line
it.line(532, 108, 532, 440); // Vertical second line
// Sunrise / Sunset
it.printf(5, 40, id(materialdesign_icons_25), TextAlign::BASELINE_LEFT, "\U000F059C");
it.printf(35, 40, id(openSansBold_font), TextAlign::BOTTOM_LEFT, id(epaper_sunrise).state.c_str());
it.printf(5, 70, id(materialdesign_icons_25), TextAlign::BASELINE_LEFT, "\U000F059B");
it.printf(35, 70, id(openSansBold_font), TextAlign::BOTTOM_LEFT, id(epaper_sunset).state.c_str());
// Temperature inside / outside
it.printf(100, 35, id(materialdesign_icons_25), TextAlign::BASELINE_LEFT, "\U000F0F54");
//it.printf(130, 40, id(openSansBold_font), TextAlign::BOTTOM_LEFT, "%s °C", id(temperature_inside).state.c_str()); // use your own temp sensor
it.printf(100, 65, id(materialdesign_icons_25), TextAlign::BASELINE_LEFT, "\U000F0F55");
//it.printf(130, 70, id(openSansBold_font), TextAlign::BOTTOM_LEFT, "%s °C", id(temperature_outside).state.c_str());// use your own temp sensor
//Time
it.strftime(540, 100, id(watch_font),TextAlign::BOTTOM_LEFT, "%H:%M", id(time_homeassistant).now());
it.strftime(540, 110, id(openSansBold_font),TextAlign::BOTTOM_LEFT, "%A, %d.%m.%y, KW %W", id(time_homeassistant).now());
// Current weather
// https://www.home-assistant.io/integrations/weather/
// weahtericon;temperature;humidity,pressure;wind_speed
// ex. data: cloudy;5,9;50;1025,6;16,9
std::map <std::string, std::string> weatherMap = {
std::make_pair("exceptional","\U000F0F2F"),
std::make_pair("cloudy","\U000F0590"),
std::make_pair("cloudy-alert","\U000F0F2F"),
std::make_pair("fog","\U000F0591"),
std::make_pair( "hail","\U000F0592"),
std::make_pair( "hazy","\U000F0F30"),
std::make_pair( "hurricane","\U000F0898"),
std::make_pair( "lightning","\U000F0593"),
std::make_pair( "lightning-rainy","\U000F067E"),
std::make_pair( "night","\U000F0594"),
std::make_pair( "clear-night","\U000F0594"),
std::make_pair( "night-partly-cloudy","\U000F0F31"),
std::make_pair( "partly-cloudy","\U000F0595"),
std::make_pair( "partlycloudy","\U000F0595"),
std::make_pair( "partly-lightning","\U000F0F32"),
std::make_pair( "partly-rainy","\U000F0F33"),
std::make_pair( "partly-snowy","\U000F0F34"),
std::make_pair( "partly-snowy-rainy","\U000F0F35"),
std::make_pair( "pouring","\U000F0596"),
std::make_pair( "rainy","\U000F0597"),
std::make_pair( "snowy","\U000F0598"),
std::make_pair( "snowy-heavy","\U000F0F36"),
std::make_pair( "snowy-rainy","\U000F067F"),
std::make_pair( "sunny","\U000F0599"),
std::make_pair( "sunny-alert","\U000F0F37"),
std::make_pair( "sunny-off","\U000F14E4"),
std::make_pair( "tornado","\U000F0F38"),
std::make_pair( "windy","\U000F059D"),
std::make_pair( "windy-variant","\U000F059E")};
if(id(epaper_weather_actual).has_state()) {
std::string actualWeatherCSV = id(epaper_weather_actual).state; //("cloudy;5,9;50;1025,6;16,9");
ESP_LOGI("Weather today", "%s", actualWeatherCSV.c_str());
std::size_t current, previous = 0;
char delim = ';';
current = actualWeatherCSV.find(delim);
x = 5; y = 200;
for (int i=0; i<5; i++) {
const char * value = actualWeatherCSV.substr(previous, current - previous).c_str();
if(i == 0) { // icon
std::string unicodeWeatherIcon = weatherMap.at(value);
it.printf(x , y, id(materialdesign_icons_50), TextAlign::BASELINE_LEFT, unicodeWeatherIcon.c_str());
y = 120;
x = x + 90;
} else if (i == 1) { // temperature
it.printf(x, y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s °C", value);
} else if (i == 2) { // humidity
it.printf(x, y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s %s", value, "%");
} else if (i == 3) { // pressure
it.printf(x, y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s PA", value);
} else if (i == 4) { // windpeed
it.printf(x, y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s m/s", value);
}
y += 25;
previous = current + 1;
current = actualWeatherCSV.find(delim, previous);
}
}
// Weather Forecast
if(id(epaper_weather_forecast).has_state()) {
std::string forecastWeatherCSV = id(epaper_weather_forecast).state; // Mon;sunny;16,5;5,1;Tue;sunny;17,2;6,8;Wed;sunny;15,4;2,7;Thu;sunny;15,3;4,6;Fri;partlycloudy;16,0;3,5
ESP_LOGI("Weather forecast", "%s", forecastWeatherCSV.c_str());
std::size_t current, previous = 0;
char delim = ';';
current = forecastWeatherCSV.find(delim);
x = 5; y = 270;
int nextRow =0;
for (int i=0; i<20; i++) {
const char * value = forecastWeatherCSV.substr(previous, current - previous).c_str();
if(i == 0 || i == 4 || i == 8 || i == 12 || i == 16) { // Weekday
it.printf(x, y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s", value);
} else if (i == 1 || i == 5 || i == 9 || i == 13 || i == 17) { // icon
it.printf(x + 50 , y, id(materialdesign_icons_32), TextAlign::BASELINE_LEFT, weatherMap.at(value).c_str());
} else if (i == 2 || i == 6 || i == 10 || i == 14 || i == 18) { // Temp high
it.printf(x + 100 , y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s°C", value);
} else if (i == 3 || i == 7 || i == 11 || i == 15 || i == 19) { // Temp low
it.printf(x + 170, y, id(openSansBold_font), TextAlign::BASELINE_LEFT,"%s °C", value);
}
nextRow = nextRow + 1;
if (nextRow == 4) {
y += 28;
nextRow = 0;
}
previous = current + 1;
current = forecastWeatherCSV.find(delim, previous);
}
}
// -- Footer ---
//IP Adress
if (id(ip_address).has_state()) {
it.printf(5, 470, id(openSansBold_font), TextAlign::BASELINE_LEFT, "IP: %s", id(ip_address).state.c_str());
}
// WiFi Signal Strength
if(id(wifisignal).has_state()) {
x = 210, y = 475;
if (id(wifisignal).state >= -50) {
it.print(x, y, id(materialdesign_icons_32), TextAlign::BOTTOM_RIGHT, "\U000F0928");
ESP_LOGI("WiFi", "Exellent");
} else if (id(wifisignal).state >= -60) {
it.print(x, y, id(materialdesign_icons_32), TextAlign::BOTTOM_RIGHT, "\U000F0925");
ESP_LOGI("WiFi", "Good");
} else if (id(wifisignal).state >= -67) {
it.print(x, y, id(materialdesign_icons_32), TextAlign::BOTTOM_RIGHT, "\U000F0922");
ESP_LOGI("WiFi", "Fair");
} else if (id(wifisignal).state >= -70) {
it.print(x, y, id(materialdesign_icons_32), TextAlign::BOTTOM_RIGHT, "\U000F091F");
ESP_LOGI("WiFi", "Weak");
} else {
it.print(x, y, id(materialdesign_icons_32), TextAlign::BOTTOM_RIGHT, "\U000F092B");
ESP_LOGI("WiFi", "Unlikely");
}
}
// ESP Home UpTime
if (id(uptime_human).has_state()) {
it.printf(520, 470, id(openSansBold_font), TextAlign::BASELINE_LEFT, "UpTime: %s", id(uptime_human).state.c_str());
}
# --- Sensors ------------------------------------------------------------------
sensor:
# ESP Home UpTime
- platform: uptime
id: uptime_sensor
update_interval: 60s
on_raw_value:
then:
- text_sensor.template.publish:
id: uptime_human
state: !lambda |-
int seconds = round(id(uptime_sensor).raw_state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
return (
(days ? String(days) + ":" : "000:") +
(hours ? String(hours) + ":" : "00:") +
(minutes ? String(minutes) + ":" : "00:") +
(String(seconds) + "")
).c_str();
- platform: wifi_signal
id: wifisignal
update_interval: 60s
text_sensor:
# ESP WLAN IP Address
- platform: wifi_info
ip_address:
name: "${name} IP Address"
id: ip_address
# ESP Home UpTime
- platform: template
id: uptime_human
icon: mdi:clock-start
# Weather forecast
- platform: homeassistant
id: epaper_weather_actual
entity_id: sensor.epaper_weather_actual
internal: true
- platform: homeassistant
id: epaper_weather_forecast
entity_id: sensor.epaper_weather_forecast
internal: true
- platform: homeassistant
id: epaper_sunrise
entity_id: sensor.epaper_sunrise
internal: true
- platform: homeassistant
id: epaper_sunset
entity_id: sensor.epaper_sunset
internal: true
time:
- platform: homeassistant
id: time_homeassistant