Battery Driven Epaper Display showing Weather Data

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

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 :slight_smile:

6 Likes

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.