Skip to content

Working setup including partial refresh #31

Description

@jnettels

I wanted to replicate what I saw here but had to make a few adjustments, based on various sources around the web. Before I start customizing my wall panel, I wanted to share the "simple" setup that currently works fine:

Components:
Waveshare 7.5 Inch E-Paper Display Hat Module V2
https://www.amazon.de/dp/B075R4QY3L
Supports esphome model "7.50inV2p" for partial refresh. This is very nice, because it is now set up as a clock that updates every minute. Only the pixels of the digits update. (Before, the whole screen flashed several times on every update, which would have been very annoying as a clock.)
However, that still needs full refresh of the whole screen to prevent burn-in. I do not know the required time interval, currently it is set to every 30th refresh, i.e. to 30 minutes. (I only had it for two days, so I do not know if that is a good value.)

The busy pin on the 7.5v2 screens needs to be inverted (as a configuration setting in the file below), according to esphome/issues#4739 (comment).

Waveshare Universal e-Paper Driver Board with WiFi SoC ESP32
https://www.amazon.de/dp/B07M5CNP3B
Set the physical "mode switch" on the board to "A" (see https://www.waveshare.com/wiki/E-Paper_ESP32_Driver_Board)

All of the above helps to prevent timeout errors (for refreshes) that were reported with other configurations.

My personal to do: Clear screen when "shutdown" is pressed in home assistant, so that the panel can savely be stored with a completely white screen.

My version of the weatherman.yaml that needs to be flashed to the ESP board:

# WEATHERMAN DASHBOARD
# For Home Assistant and ESPHome
# Designed by Madelena Mak 2022 - https://mmak.es

# Components:
# Waveshare 7.5 Inch E-Paper Display Hat Module V2
# https://www.amazon.de/dp/B075R4QY3L
# Supports esphome model "7.50inV2p" for partial refresh,
# but that still needs full refresh of the whole screen
# to prevent burn-in. I do not know the required time
# interval, currently it is set to 600 seconds.

# The busy pin on the 7.5v2 screens needs to be inverted
# https://github.com/esphome/issues/issues/4739#issuecomment-1659939636

# Waveshare Universal e-Paper Driver Board with WiFi SoC ESP32
# https://www.amazon.de/dp/B07M5CNP3B
# Set the physical "mode switch" on the board to "A"
# https://www.waveshare.com/wiki/E-Paper_ESP32_Driver_Board

# To do: Clear screen when "shutdown" is pressed in home assistant

# Cue "Blame it on the Weatherman" by B*Witched!
esphome:
  name: esphome-web-0bdd98
  friendly_name: weatherman
  on_boot:
      priority: 200.0
      then:
        - component.update: eink_display
        - wait_until:
            condition:
              lambda: 'return id(data_updated) == true;'
              # Wait a bit longer so all the items are received
        - delay: 5s
        - logger.log: "Initial sensor data received: Refreshing display..."
        - lambda: 'id(initial_data_received) = true;'
        - script.execute: update_screen

esp32:
  board: esp32dev
  framework:
    type: arduino


# Enable logging
logger: 

# Enable Home Assistant API
api:

ota:
  - platform: esphome


button:
  - platform: shutdown
    name: "Weatherman - Shutdown"
  - platform: restart
    name: "Weatherman - Restart"
  - platform: template
    name: "Weatherman - Refresh Screen"
    entity_category: config
    on_press:
      - script.execute: update_screen      
      
# Global variables for detecting if the display needs to be refreshed. (Thanks @paviro!)
globals:
  - id: data_updated
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: initial_data_received
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: recorded_display_refresh
    type: int
    restore_value: yes
    initial_value: '0'


# Script for updating screen - Refresh display and publish refresh count and time. (Thanks @paviro!)
script:
  - id: update_screen
    then:
      - lambda: 'id(data_updated) = false;'
      - component.update: eink_display
      - lambda: 'id(recorded_display_refresh) += 1;'
      - lambda: 'id(display_last_update).publish_state(id(homeassistant_time).now().timestamp);'
      

# Check whether the display needs to be refreshed every minute,
# based on whether new data is received or motion is detected. (Thanks @paviro!)
time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      - seconds: 0
        minutes: /1
        then:
          - script.execute: update_screen
          - if:
              condition:
                lambda: 'return id(data_updated) == true;'
              then:
                - if:
                    condition:
                      binary_sensor.is_on: motion_detected
                    then:
                      - logger.log: "Sensor data updated and activity in home detected: Refreshing display..."
                      # - script.execute: update_screen
                    else:
                      - logger.log: "Sensor data updated but no activity in home - skipping display refresh."
              else:
                - logger.log: "No sensors updated - skipping display refresh."


# Wifi information
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: !secret weatherman_ap_ssid
    password: !secret weatherman_ap_password


# Include custom fonts
font:
  - file: 'fonts/GothamRnd-Book.ttf'
    id: font_small_book
    size: 18
  - file: 'fonts/GothamRnd-Bold.ttf'
    id: font_large_bold
    size: 108
    glyphs: [' ', '-', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C']
  - file: 'fonts/GothamRnd-Bold.ttf'
    id: font_title
    size: 54
    glyphs: ['W', 'E', 'A', 'T', 'H', 'R', 'L', 'I', 'N', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'U', 'h', 'r', '.', ':']
  - file: 'fonts/GothamRnd-Bold.ttf'
    id: font_medium_bold
    size: 30
    # glyphs: [' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'M', 'I', 'N']
  - file: 'fonts/GothamRnd-Bold.ttf'
    id: font_small_bold
    size: 18
    # glyphs: [' ', '-', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', 'M', 'I', 'N']


  # Include Material Design Icons font
  # Thanks to https://community.home-assistant.io/t/display-materialdesign-icons-on-esphome-attached-to-screen/199790/16
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_mdi_large
    size: 96
    glyphs: &mdi-weather-glyphs
      - "\U000F0590" # mdi-weather-cloudy
      - "\U000F0F2F" # mdi-weather-cloudy-alert
      - "\U000F0E6E" # mdi-weather-cloudy-arrow-right
      - "\U000F0591" # mdi-weather-fog
      - "\U000F0592" # mdi-weather-hail
      - "\U000F0F30" # mdi-weather-hazy
      - "\U000F0898" # mdi-weather-hurricane
      - "\U000F0593" # mdi-weather-lightning
      - "\U000F067E" # mdi-weather-lightning-rainy
      - "\U000F0594" # mdi-weather-clear-night
      - "\U000F0F31" # mdi-weather-night-partly-cloudy
      - "\U000F0595" # mdi-weather-partly-cloudy
      - "\U000F0F32" # mdi-weather-partly-lightning
      - "\U000F0F33" # mdi-weather-partly-rainy
      - "\U000F0F34" # mdi-weather-partly-snowy
      - "\U000F0F35" # mdi-weather-partly-snowy-rainy
      - "\U000F0596" # mdi-weather-pouring
      - "\U000F0597" # mdi-weather-rainy
      - "\U000F0598" # mdi-weather-snowy
      - "\U000F0F36" # mdi-weather-snowy-heavy
      - "\U000F067F" # mdi-weather-snowy-rainy
      - "\U000F0599" # mdi-weather-sunny
      - "\U000F0F37" # mdi-weather-sunny-alert
      - "\U000F14E4" # mdi-weather-sunny-off
      - "\U000F059A" # mdi-weather-sunset
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up
      - "\U000F0F38" # mdi-weather-tornado
      - "\U000F059D" # mdi-weather-windy
      - "\U000F059E" # mdi-weather-windy-variant
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_mdi_medium
    size: 36
    glyphs: *mdi-weather-glyphs


# Include Custom Titles
# image:
#   - file: "images/weatherman-title-train.png"
#     id: title_train
#     type: BINARY
#   - file: "images/weatherman-title-weather.png"
#     id: title_weather
#     type: BINARY


# Check if motion is detected in the living room.
binary_sensor:
  - platform: homeassistant
    entity_id: binary_sensor.weatherman_motion_detected
    id: motion_detected


sensor:
  # Create sensors for monitoring Weatherman remotely.
  - platform: template
    name: "Weatherman - Display Last Update"
    device_class: timestamp
    entity_category: "diagnostic"
    id: display_last_update
    
  - platform: template
    name: "Weatherman - Recorded Display Refresh"
    accuracy_decimals: 0
    unit_of_measurement: "Refreshes"
    state_class: "total_increasing"
    entity_category: "diagnostic"
    lambda: 'return id(recorded_display_refresh);'
  
  - platform: wifi_signal
    name: "Weatherman - WiFi Signal Strength"
    id: wifisignal
    unit_of_measurement: "dBm"
    entity_category: "diagnostic"
    update_interval: 60s

  # Call Subway and Weather sensors from HA.
  - platform: homeassistant
    entity_id: weather.forecast_home
    attribute: temperature
    id: weather_temperature
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'

  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_temperature_0
    id: weather_temperature_0
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'

  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_temperature_1
    id: weather_temperature_1
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'

  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_temperature_2
    id: weather_temperature_2
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'

  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_temperature_3
    id: weather_temperature_3
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'


text_sensor:
  - platform: homeassistant
    entity_id: weather.forecast_home
    id: weather_state
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'

  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_condition_now
    id: weather_condition_now
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_condition_0
    id: weather_condition_0
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_timestamp_0
    id: weather_timestamp_0
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_condition_1
    id: weather_condition_1
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_timestamp_1
    id: weather_timestamp_1
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_condition_2
    id: weather_condition_2
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_timestamp_2
    id: weather_timestamp_2
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_condition_3
    id: weather_condition_3
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'
  - platform: homeassistant
    entity_id: sensor.weatherman_data
    attribute: weather_timestamp_3
    id: weather_timestamp_3
    on_value:
      then:
        - lambda: 'id(data_updated) = true;'


# Define colors
# This design is white on black so this is necessary.
color:
  - id: color_bg
    red: 0%
    green: 0%
    blue: 0%
    white: 0%
  - id: color_text
    red: 0%
    green: 0%
    blue: 0%
    white: 100%


# Pins for Waveshare ePaper ESP Board
spi:
  clk_pin: GPIO13
  mosi_pin: GPIO14


# Now render everything on the ePaper screen.
display:
  - platform: waveshare_epaper
    id: eink_display
    cs_pin: GPIO15
    dc_pin: GPIO27
    busy_pin: 
      number: GPIO25
      inverted: True
    reset_pin: GPIO26
    reset_duration: 2ms
    model: 7.50inV2p
    update_interval: never
    full_update_every: 30
    rotation: 90°
    lambda: |-
      // Map weather states to MDI characters.
      std::map<std::string, std::string> weather_icon_map
        {
          {"cloudy", "\U000F0590"},
          //{"cloudy-alert", "\U000F0F2F"},
          //{"cloudy-arrow-right", "\U000F0E6E"},
          {"fog", "\U000F0591"},
          {"hail", "\U000F0592"},
          //{"hazy", "\U000F0F30"},
          //{"hurricane", "\U000F0898"},
          {"lightning", "\U000F0593"},
          {"lightning-rainy", "\U000F067E"},
          {"clear-night", "\U000F0594"},
          {"night-partly-cloudy", "\U000F0F31"},
          {"partlycloudy", "\U000F0595"},
          //{"partly-lightning", "\U000F0F32"},
          {"partly-rainy", "\U000F0F33"},
          {"partly-snowy", "\U000F0F34"},
          {"partly-snowy-rainy", "\U000F0F35"},
          {"pouring", "\U000F0596"},
          {"rainy", "\U000F0597"},
          {"snowy", "\U000F0598"},
          {"snowy-heavy", "\U000F0F36"},
          {"snowy-rainy", "\U000F067F"},
          {"sunny", "\U000F0599"},
          //{"sunny-alert", "\U000F0F37"},
          //{"sunny-off", "\U000F14E4"},
          //{"sunset", "\U000F059A"},
          //{"sunset-down", "\U000F059B"},
          //{"sunset-up", "\U000F059C"},
          //{"tornado", "\U000F0F38"},
          {"windy", "\U000F059D"},
          {"windy-variant", "\U000F059E"},
        };

      // Fill background.
      // it.fill(color_bg);

      // Show loading screen before data is received.
      if (id(initial_data_received) == false) {
        it.printf(240, 390, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "WARTE AUF DATEN...");
      } else {

        // Weather Section
        // it.image(0, 88, id(title_weather)); 
        it.printf(240, 84, id(font_title), color_text, TextAlign::TOP_CENTER, "WETTER");

        it.printf(100, 158, id(font_mdi_large), color_text, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_now).state.c_str()].c_str());

        it.printf(300, 158, id(font_large_bold), color_text, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature).state);

        it.printf(105, 282, id(font_small_book), color_text, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_0).state.c_str());
        it.printf(105, 306, id(font_mdi_medium), color_text, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_0).state.c_str()].c_str());
        it.printf(105, 354, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_0).state);

        it.printf(195, 282, id(font_small_book), color_text, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_1).state.c_str());
        it.printf(195, 306, id(font_mdi_medium), color_text, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_1).state.c_str()].c_str());
        it.printf(195, 354, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_1).state);

        it.printf(285, 282, id(font_small_book), color_text, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_2).state.c_str());
        it.printf(285, 306, id(font_mdi_medium), color_text, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_2).state.c_str()].c_str());
        it.printf(285, 354, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_2).state);

        it.printf(375, 282, id(font_small_book), color_text, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_3).state.c_str());
        it.printf(375, 306, id(font_mdi_medium), color_text, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_3).state.c_str()].c_str());
        it.printf(375, 354, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_3).state);

        // Clock
        char str_time[17];
        char str_date[17];
        time_t currTime = id(homeassistant_time).now().timestamp;
        strftime(str_time, sizeof(str_time), "%H:%M", localtime(&currTime));
        strftime(str_date, sizeof(str_date), "%d.%m.%Y", localtime(&currTime));
        it.printf(240, 408, id(font_title), color_text, TextAlign::TOP_CENTER, "%s Uhr", str_time);
        it.printf(240, 460, id(font_title), color_text, TextAlign::TOP_CENTER, "%s", str_date);

        // Train Service Section
        // it.image(0, 420, id(title_train)); 
        //it.printf(240, 408, id(font_title), color_text, TextAlign::TOP_CENTER, "L TRAIN");

        //it.print(150, 524, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "TO MANHATTAN");
        //it.print(330, 524, id(font_small_bold), color_text, TextAlign::TOP_CENTER, "TO CANARSIE");

        // Refresh Timestamp
        // Code by EnsconcE from https://community.home-assistant.io/t/esphome-show-time/348903
        it.printf(240, 710, id(font_small_book), color_text, TextAlign::TOP_CENTER, "REFRESHED AT %s", str_time);
      }

captive_portal:

And this is the snippet of code that needs to be pasted into /homeassistant/configuration.yaml

template:

  # Update screen only when occupancy is detected.

  - binary_sensor:
      - name: Weatherman Motion Detected
        unique_id: "dfa78de7-d761-425f-9731-86f1af332eac"
        device_class: "occupancy"
        delay_off: 1min
        state: >-
          {%- if states('binary_sensor.0x8c65a3fffe359450_occupancy') == 'on'%}
            on
          {%- else -%}
            off
          {%- endif -%}


  # Bundle up all the data to send over to Weatherman (ESPHome device).

  - trigger:
      platform: time_pattern
      minutes: "/1"
    action:
      - service: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.forecast_home
        response_variable: hourly
    sensor:
      - name: Weatherman Data
        state: "OK"
        attributes:
          weather_condition_now: >
            {% set cond_now = states('weather.forecast_home') %}
            {% if states('sun.sun') == 'below_horizon' %}
                {% if cond_now == 'sunny' %} night {% elif cond_now == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond_now }} {% endif %}
            {% else %}
                {{ cond_now }}
            {% endif %}
            
          weather_condition_0: >
            {% set cond0 = hourly['weather.forecast_home'].forecast[0].condition %}
            {% set next_setting = as_timestamp(state_attr('sun.sun', 'next_setting')) %}
            {% set next_rising = as_timestamp(state_attr('sun.sun', 'next_rising')) %}
            {% set cond0_time = as_timestamp(hourly['weather.forecast_home'].forecast[0].datetime) %}
            {% if cond0_time > next_setting or cond0_time < next_rising < next_setting %}
                {% if cond0 == 'sunny' %} night {% elif cond0 == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond0 }} {% endif %}
            {% else %}
                {{ cond0 }}
            {% endif %}
          weather_temperature_0: >
            {{ hourly['weather.forecast_home'].forecast[0].temperature | round }}
          weather_timestamp_0: >
            {{ as_timestamp(hourly['weather.forecast_home'].forecast[0].datetime) | timestamp_custom('%H:%M') }}


          weather_condition_1: >
            {% set cond0 = hourly['weather.forecast_home'].forecast[2].condition %}
            {% set next_setting = as_timestamp(state_attr('sun.sun', 'next_setting')) %}
            {% set next_rising = as_timestamp(state_attr('sun.sun', 'next_rising')) %}
            {% set cond0_time = as_timestamp(hourly['weather.forecast_home'].forecast[2].datetime) %}
            {% if cond0_time > next_setting or cond0_time < next_rising < next_setting %}
                {% if cond0 == 'sunny' %} night {% elif cond0 == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond0 }} {% endif %}
            {% else %}
                {{ cond0 }}
            {% endif %}
          weather_temperature_1: >
            {{ hourly['weather.forecast_home'].forecast[2].temperature | round }}
          weather_timestamp_1: >
            {{ as_timestamp(hourly['weather.forecast_home'].forecast[2].datetime) | timestamp_custom('%H:%M') }}

            
          weather_condition_2: >
            {% set cond0 = hourly['weather.forecast_home'].forecast[4].condition %}
            {% set next_setting = as_timestamp(state_attr('sun.sun', 'next_setting')) %}
            {% set next_rising = as_timestamp(state_attr('sun.sun', 'next_rising')) %}
            {% set cond0_time = as_timestamp(hourly['weather.forecast_home'].forecast[4].datetime) %}
            {% if cond0_time > next_setting or cond0_time < next_rising < next_setting %}
                {% if cond0 == 'sunny' %} night {% elif cond0 == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond0 }} {% endif %}
            {% else %}
                {{ cond0 }}
            {% endif %}
          weather_temperature_2: >
            {{ hourly['weather.forecast_home'].forecast[4].temperature | round }}
          weather_timestamp_2: >
            {{ as_timestamp(hourly['weather.forecast_home'].forecast[4].datetime) | timestamp_custom('%H:%M') }}

          weather_condition_3: >
            {% set cond0 = hourly['weather.forecast_home'].forecast[6].condition %}
            {% set next_setting = as_timestamp(state_attr('sun.sun', 'next_setting')) %}
            {% set next_rising = as_timestamp(state_attr('sun.sun', 'next_rising')) %}
            {% set cond0_time = as_timestamp(hourly['weather.forecast_home'].forecast[6].datetime) %}
            {% if cond0_time > next_setting or cond0_time < next_rising < next_setting %}
                {% if cond0 == 'sunny' %} night {% elif cond0 == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond0 }} {% endif %}
            {% else %}
                {{ cond0 }}
            {% endif %}
          weather_temperature_3: >
            {{ hourly['weather.forecast_home'].forecast[6].temperature | round }}
          weather_timestamp_3: >
            {{ as_timestamp(hourly['weather.forecast_home'].forecast[6].datetime) | timestamp_custom('%H:%M') }}

Cheers, hope this helps you!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions