In Openhab I am collecting quite some weather data, e.g. indoor and outdoor temperature, humidity, pressure and furthermore the weather forecast as well as sunrise and sunset times. I would like to show this data at an epaper display which should be battery (LiPo) driven. I searched in several forums and found a nice solution with home assistant and esphome.
https://community.home-assistant.io/t/air-quality-sensors-e-ink-display-using-esphome/201776
A nice housing is still missing.
I would like to share with you an adaptation to Openhab and extension to operate it with battery. What do you need:
- Esp32doit-devkit-v1
- Waveshare epaper 4.2“ display
- If you want to to operate it via battery you need a battery shield and LiPo battery.
Showing the content at the display is fairly easy with esphome, even with icons and your preferred fonts. You need to download the following fonts and put them in a „fonts“ subfolder
- Material icons (https://materialdesignicons.com) as a ttf
- Kanit font (Google Fonts)
Connect the epaper display to your Esp32 using the pins described in the code below. Make sure the epaper display interface is configured as 4-wire SPI because esphome does not support 3-wire.
Epaper displays do not need power to display any content, only updates require energy - that’s well suited for battery operation. Therefore, I update my display quite seldom, once every hour and showing a time at the display does not make any sense. All relevant data is published by Openhab via MQTT. Basically you can choose to trigger the display update via Openhab using a time based rule (cron) or the Esp32 can request its data from Openhab. Triggering the update via Openhab and MQTT is not a good idea for battery driven devices because you want to use deep sleep in order to save battery. During deep sleep messages will not arrive at your Esp32. Therefore I realized a req-rep interface: the Esp32 publishes a birth MQTT message as soon as it connects to the broker (request). This message is processed by a rule which collects and publishes all relevant data via MQTT (the Esp32 subscribes these topics) and Openhab finally publishes an update signal (dedicated topic). After that, the Esp32 falls asleep and wakes up an hour later.
That’s the esphome code - replace the MQTT topics and the wifi credentials:
substitutions:
devicename: display
gpio_spi_clk_pin: GPIO18
gpio_spi_mosi_pin: GPIO23
gpio_cs_pin: GPIO05
gpio_busy_pin: GPIO02
gpio_reset_pin: GPIO15
gpio_dc_pin: GPIO13
esphome:
name: display
platform: ESP32
board: esp32doit-devkit-v1
spi:
clk_pin: $gpio_spi_clk_pin
mosi_pin: $gpio_spi_mosi_pin
id: epaper_display
time:
- platform: sntp
timezone: Europe/Paris
id: sntp_time
deep_sleep:
run_duration: 60s
sleep_duration: 60min
id: deep_sleep_1
mqtt:
broker: my.broker.ip.address
discovery: True
username: xxxx
password: xxxx
birth_message:
topic: cmnd/openhab/sensors/update
payload: "Update"
on_message:
- topic: cmnd/display/ota_mode
payload: 'ON'
then:
- deep_sleep.prevent: deep_sleep_1
- topic: cmnd/display/sleep_mode
payload: 'ON'
then:
- deep_sleep.enter: deep_sleep_1
text_sensor:
- platform: mqtt_subscribe
name: "Temperature"
id: indoor_temp
topic: tele/openhab/sensors/indoor_temp
- platform: mqtt_subscribe
name: "Humidity"
id: indoor_hum
topic: tele/openhab/sensors/indoor_hum
- platform: mqtt_subscribe
name: "Pressure"
id: pressure
topic: tele/openhab/sensors/pressure
- platform: mqtt_subscribe
name: "Outdoor Temperature"
id: outdoor_temp
topic: tele/openhab/sensors/outdoor_temp
- platform: mqtt_subscribe
name: "Outdoor Humidity"
id: outdoor_hum
topic: tele/openhab/sensors/outdoor_hum
- platform: mqtt_subscribe
name: "Weather"
id: weather
topic: tele/openhab/sensors/weather
- platform: mqtt_subscribe
name: "Sunse"
id: sunset
topic: tele/openhab/sensors/sunset
- platform: mqtt_subscribe
name: "Sunrise"
id: sunrise
topic: tele/openhab/sensors/sunrise
- platform: mqtt_subscribe
name: "Max. Temp"
id: max_temp
topic: tele/openhab/sensors/weather_maxtemp
- platform: mqtt_subscribe
name: "Min. Temp"
id: min_temp
topic: tele/openhab/sensors/weather_mintemp
- platform: mqtt_subscribe
name: "Display Control"
id: display_control
topic: cmnd/display/update
on_value:
- component.update: epaper
display:
- platform: waveshare_epaper
id: epaper
cs_pin: $gpio_cs_pin
busy_pin: $gpio_busy_pin
reset_pin: $gpio_reset_pin
dc_pin: $gpio_dc_pin
model: 4.20in # 300x400
rotation: 270°
update_interval: never
lambda: |-
ESP_LOGI("display", "Updating...");
// OUTSIDE
it.printf(7, 15, id(font_medium_20), TextAlign::BASELINE_LEFT, "Outside");
it.line(90, 14, 293, 14);
it.printf(25, 67, id(icon_font_35), TextAlign::BASELINE_CENTER, "\U000F058E");
if (id(outdoor_hum).has_state()) {
it.printf(41, 66, id(font_regular_35), TextAlign::BASELINE_LEFT, "%s", id(outdoor_hum).state.c_str());
it.printf(95, 65, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
}
it.printf(145, 65, id(icon_font_40), TextAlign::BASELINE_CENTER, "\U000F050F");
if (id(outdoor_temp).has_state()) {
it.printf(220, 65, id(font_regular_45), TextAlign::BASELINE_CENTER, "%s°", id(outdoor_temp).state.c_str());
}
// INSIDE
it.printf(7, 110, id(font_medium_20), TextAlign::BASELINE_LEFT, "Inside");
it.line(81, 109, 293, 109);
it.printf(25, 161, id(icon_font_25), TextAlign::BASELINE_CENTER, "\U000F058E");
if (id(indoor_hum).has_state()) {
it.printf(41, 160, id(font_regular_35), TextAlign::BASELINE_LEFT, "%s", id(indoor_hum).state.c_str());
it.printf(90, 160, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
}
it.printf(145, 159, id(icon_font_40), TextAlign::BASELINE_CENTER, "\U000F02DC");
if (id(indoor_temp).has_state()) {
it.printf(220, 160, id(font_regular_45), TextAlign::BASELINE_CENTER, "%s°", id(indoor_temp).state.c_str());
}
it.printf(25, 211, id(icon_font_25), TextAlign::BASELINE_CENTER, "\U000F029A");
if (id(pressure).has_state()) {
it.printf(120, 210, id(font_regular_35), TextAlign::BASELINE_RIGHT, "%s", id(pressure).state.c_str());
it.printf(123, 210, id(font_regular_30), TextAlign::BASELINE_LEFT, "hPa");
}
// WEATHER
it.printf(15, 245, id(font_medium_20), TextAlign::BASELINE_LEFT, "Weather");
it.line(100, 244, 293, 244);
if (id(weather).has_state()) {
std::map<std::string, std::string> weather_state
{
{"01d", "\U000F0599"}, // clear sky - mdi-weather-sunny
{"02d", "\U000F0595"}, // few clouds - mdi-weather-partly-cloudy
{"03d", "\U000F0590"}, // scattered clouds - mdi-weather-cloudy
{"04d", "\U000F0590"}, // broken clouds - mdi-weather-cloudy
{"09d", "\U000F0596"}, // shower rain - mdi-weather-pouring
{"10d", "\U000F0597"}, // rain - mdi-weather-partly-rainy
{"11d", "\U000F067E"}, // thunderstorm - mdi-weather-lightning-rainy
{"13d", "\U000F0598"}, // snow - mdi-weather-snowy
{"50d", "\U000F0591"}, // mist - mdi-weather-fog
{"01n", "\U000F0599"}, // clear sky - mdi-weather-sunny
{"02n", "\U000F0595"}, // few clouds - mdi-weather-partly-cloudy
{"03n", "\U000F0590"}, // scattered clouds - mdi-weather-cloudy
{"04n", "\U000F0590"}, // broken clouds - mdi-weather-cloudy
{"09n", "\U000F0596"}, // shower rain - mdi-weather-rainy
{"10n", "\U000F0597"}, // rain - mdi-weather-partly-rainy
{"11n", "\U000F067E"}, // thunderstorm - mdi-weather-lightning-rainy
{"13n", "\U000F0598"}, // snow - mdi-weather-snowy
{"50n", "\U000F0591"} // mist - mdi-weather-fog
};
it.printf(60, 305, id(icon_font_40), TextAlign::BASELINE_CENTER, weather_state[id(weather).state.c_str()].c_str());
}
it.printf(145, 280, id(icon_font_25), TextAlign::BASELINE_CENTER, "\U000F10C2"); // temp-high
it.printf(145, 320, id(icon_font_25), TextAlign::BASELINE_CENTER, "\U000F10C3"); // temp-low
if (id(min_temp).has_state()) {
it.printf(220, 280, id(font_regular_35), TextAlign::BASELINE_CENTER, "%s°", id(max_temp).state.c_str());
it.printf(220, 320, id(font_regular_35), TextAlign::BASELINE_CENTER, "%s°", id(min_temp).state.c_str());
}
// TIME
it.line(7, 327, 293, 327);
it.strftime(7, 353, id(font_medium_20), TextAlign::BASELINE_LEFT, "%A", id(sntp_time).now());
it.strftime(7, 383, id(font_medium_20), TextAlign::BASELINE_LEFT, "%d %b. %Y", id(sntp_time).now());
it.printf(145, 358, id(icon_font_25), TextAlign::BASELINE_CENTER, "\U000F059C"); // sunrise
it.printf(145, 388, id(icon_font_25), TextAlign::BASELINE_CENTER, "\U000F059B"); // sunset
if (id(sunset).has_state()) {
it.printf(220, 353, id(font_medium_20), TextAlign::BASELINE_CENTER, "%s", id(sunrise).state.c_str());
it.printf(220, 383, id(font_medium_20), TextAlign::BASELINE_CENTER, "%s", id(sunset).state.c_str());
}
font:
- file: 'fonts/Kanit-Medium.ttf'
id: font_medium_20
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/Kanit-Regular.ttf'
id: font_regular_30
size: 30
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/Kanit-Regular.ttf'
id: font_regular_35
size: 35
glyphs:
['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']
- file: 'fonts/Kanit-Regular.ttf'
id: font_regular_45
size: 45
glyphs:
['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']
- file: 'fonts/Kanit-Regular.ttf'
id: font_regular_65
size: 65
glyphs:
['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_20
size: 20
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F058E", # mdi-water-percent
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_25
size: 25
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F058E", # mdi-water-percent
"\U000F029A", # mdi-gauge
"\U000F059B", # mdi-weather-sunset-down
"\U000F059C", # mdi-weather-sunset-up
"\U000F10C2", # mdi-thermometer-high
"\U000F10C3" # mdi-thermometer-low
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_30
size: 30
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F058E", # mdi-water-percent
"\U000F02DC", # mdi-home
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_35
size: 35
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F058E", # mdi-water-percent
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_40
size: 40
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F058E", # mdi-water-percent
"\U000F02DC", # mdi-home
# Weather
"\U000F0599", # mdi-weather-sunny
"\U000F0590", # mdi-weather-cloudy
"\U000F0595", # mdi-weather-partly-cloudy
"\U000F0596", # mdi-weather-pouring
"\U000F0597", # mdi-weather-partly-rainy
"\U000F067E", # mdi-weather-lightning-rainy
"\U000F0598", # mdi-weather-snowy
"\U000F0591" # mdi-weather-fog
]
wifi:
ssid: "xxxx"
password: "xxxx"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Display Fallback Hotspot"
password: "xxxx"
captive_portal:
web_server:
port: 80
auth:
username: xxxx
password: xxxxx
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
Download esphome, compile the stuff with
esphome display.yaml compile
and flash the Esp32 using the esphome-flasher
Once flashed you can perform future updates via OTA:
esphome display.yaml run
If you got any problems in compiling please check if the python package pillow
is installed.
Furthermore, you might increase the run_duration
because the Esp32 should not fall asleep during an OTA update. If you publish the topic cmnd/display/ota_mode
in the rule below the Esp32 does not fall asleep.
Create a thing
Thing topic display "Display" {
Channels:
Type string : update_request "Display Update" [ stateTopic="cmnd/openhab/sensors/update"]
}
And a string item Display_UpdateRequest
The openhab rule is:
rule "Publish Data"
when
//Time cron "0 1 * * * ?"
Item Display_UpdateRequest received update
then
logInfo("Display", "Updating")
val mqttActions = getActions("mqtt","mqtt:broker:embedded-mqtt-broker")
mqttActions.publishMQTT("tele/openhab/sensors/outdoor_hum", String::format("%.0f",(OutdoorSensor_Humidity.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/outdoor_temp", String::format("%.1f",(OutdoorSensor_Temperature.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/indoor_temp", String::format("%.1f",(Sensors_Temperature.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/indoor_hum", String::format("%.0f",(Sensors_Humidity.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/pressure", String::format("%.0f",(Sensors_Pressure.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/weather", LocalWeatherAndForecast_Current_Iconid.state.toString)
mqttActions.publishMQTT("tele/openhab/sensors/weather_maxtemp", String::format("%.0f",(LocalWeatherAndForecast_ForecastToday_Maxtemperature.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/weather_mintemp", String::format("%.0f",(LocalWeatherAndForecast_ForecastToday_Mintemperature.state as Number).floatValue))
mqttActions.publishMQTT("tele/openhab/sensors/sunrise", Sonnenaufgang.state.format("%1$tH:%1$tM"))
mqttActions.publishMQTT("tele/openhab/sensors/sunset", Sonnenuntergang.state.format("%1$tH:%1$tM"))
/* Disable sleep mode */
// mqttActions.publishMQTT("cmnd/display/ota_mode", "ON")
mqttActions.publishMQTT("cmnd/display/update", "ON")
end
Have a lot of fun with it