By Stephan Wijman

Introduction

I found a blog post about a Weatherman Dashboard by Madelena Mak which made me want to have one. I haven't used ESPHome on Home Assistant yet so that was also a reason why I wanted to build one to work with ESPHome.

Looking into the E-Ink display mentioned on the Weatherman Dashboard is from WaveShare and the one used was the 7.5" Black/White version. When I did some more searching into it I also found the 7.5" Red/Black/White which give the additional red colour. The downside of this version is that the refresh time increases from 5s to 16s. I thought this is acceptable for a weather display as it won't require refreshing that often.

2023-12-28: I got informed that the YAML script below only had BLACK for colour, which when I checked was correct. I forgot to update the blog entry with the latest code. I have just updated with the current code I am running.

2024-04-10: Since Home-Assistant 2024.04 the forecast atttributes are not part of the weather entity. The YAML section has been updated to set the forecast attributes into 2 entities, one for hourly and one for daily forecast.

Material used

For this project I am using the the following 4 items:

The HOVSTA also comes in medium brown and dark brown. Instead of the HOVSTA frame you could also use the RIBBA frame which is available in white and black.

I went for the HOVSTA photo frame because it has a box frame insert, with which you can have your photo to the front, or deeper to the back. By using the box frame insert behind the matting (a border that surrounds the photo), there is space to house the ESP32 development board.
There is additional room for extra hardware like a LiPo battery if required. I won't discuss this here since it depends on the use case and which charger board or battery is preferred. So I leave that to your own choice.

Building the frame

Since I am using the box frame insert behind the matting I will need to fix the E-Ink display to the matting to make sure it doesn't move. I used normal clear tape to attach it to the matting.

As can be seen the display is larger than the opening in the matting, so part of the display can't be used. This isn't a problem for me, since there is enough space free to present the information I want.

Then place the box frame insert back in, making sure the cable from the display isn't caught between the frame and the insert.

I then push 2 of the pins down to retain the display and box frame insert.

Next step is to connect the ribbon provided with the ESP32 Driver Board. I do this because I want to mount the electronics on the backplate, to leave the display alone, to avoid any risk of damage. By using the ribbon there is more room to take the backplate off when required.

Mount ESP32 to backplate

I use a breadboard PCB to mount the ESP32 Driver Board, so I then can fix the broadboard PCB to the backplate. By using the breadboard PCB I can lower the profile, to make it fit within the frame, and I also have space left for any additional electronics like LiPo charger board. You can of course mount the ESP32 Driver Board directly on the backplate.

To minimise the space as much as possible I am going to shorten the pins of the ESP32 Driver Board before soldering it onto the breadboard PCB. Cutting them just long enough that they fit the holes but don't stick out I can make the soldering as flush as possible.

I then use double-sided tape to stick the breadboard PCB onto the backplate.

Add configuration to Home Assistant

First we need to add the required entities to Home Assistant, which then can be used by ESPHome to display on the E-Ink display.

Add motion detection entity

To reduce the refresh on the display only when somebody is present in the livingroom I added the following entity to monitor both sensors (standard motion sensor and 60GHz occupancy sensor).

This entity has to be created as part of the template configuration.

  # Update screen only when occupancy is detected.

  - binary_sensor:
      - name: Weatherframe Motion Detected
        unique_id: "dfa78de7-d761-425f-9731-86f1af332eac"
        device_class: "occupancy"
        delay_off: 00:01:00
        state: >-
          {%- if states('binary_sensor.motion_livingroom_occupancy') == 'on' or states('binary_sensor.presence_livingroom_presence') == 'on' %}
            on
          {%- else -%}
            off
          {%- endif -%}

This will need to be altered to monitor the devices you would require for your situation. If you don't care about this and have it update when actual weather data changes you can ommit this step.

Add weather data entities

The following section of the template is to populate the fields used in the ESPHome configuration. Edit these to the relevant entities within Home Assistant to populate the required data.

It is made of 3 sections:

  • Current temperature + condition
  • Hourly temperature + condition (4 hours)
  • Daily temperature and condition (4 days, today and next 3 days)
  # Bundle up all the data to send over to Weatherframe (ESPHome device).
  # For the timestamps use: %H = 24hrs / %I = 12hrs
  # To add AM/PM add the following: {{ as_timestamp(state_attr('weather.openweathermap_hourly', 'forecast')[x].datetime) | timestamp_custom('%p') }}
  - trigger:
      - platform: time_pattern
        minutes: "/30"
    action:
      - service: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.openweathermap_hourly
        response_variable: hourly
    sensor:
      - name: Weather Forecast Hourly
        unique_id: weather_forecast_hourly
        state: "{{ now().isoformat() }}"
        attributes:
          forecast: "{{ hourly['weather.openweathermap_hourly'].forecast }}"

  - trigger:
      - platform: time_pattern
        hours: "/1"
    action:
      - service: weather.get_forecasts
        data:
          type: daily
        target:
          entity_id: weather.openweathermap
        response_variable: daily
    sensor:
      - name: Weather Forecast Daily
        unique_id: weather_forecast_daily
        state: "{{ now().isoformat() }}"
        attributes:
          forecast: "{{ daily['weather.openweathermap'].forecast }}"

  - trigger:
      platform: time_pattern
      minutes: "/15"
    sensor:
      - name: Weatherframe Data
        state: "OK"
        attributes: 
          # Current temperature + condition
          weather_now_temperature: >
            {{ state_attr('weather.openweathermap', 'temperature') | round }}
          weather_now_condition: >
            {% set cond_now = states('weather.openweathermap') %}
            {% 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 %}
          # Hourly temperature + condition + timestamp (4x)
          weather_hourly_condition_0: >
            {% set cond = state_attr('sensor.weather_forecast_hourly', '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 cond_time = as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[0].datetime) %}
            {% if ((cond_time > next_setting and cond_time < next_rising) or (cond_time < next_setting and cond_time < next_rising and next_rising < next_setting)) %}
                {% if cond == 'sunny' %} night {% elif cond == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond }} {% endif %}
            {% else %}
                {{ cond }}
            {% endif %}
          weather_hourly_temperature_0: >
            {{ state_attr('sensor.weather_forecast_hourly', 'forecast')[0].temperature | round }}
          weather_hourly_timestamp_0: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[0].datetime) | timestamp_custom('%H:00') }}
          weather_hourly_condition_1: >
            {% set cond = state_attr('sensor.weather_forecast_hourly', 'forecast')[1].condition %}
            {% set next_setting = as_timestamp(state_attr('sun.sun', 'next_setting')) %}
            {% set next_rising = as_timestamp(state_attr('sun.sun', 'next_rising')) %}
            {% set cond_time = as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[1].datetime) %}
            {% if ((cond_time > next_setting and cond_time < next_rising) or (cond_time < next_setting and cond_time < next_rising and next_rising < next_setting)) %}
                {% if cond == 'sunny' %} night {% elif cond == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond }} {% endif %}
            {% else %}
                {{ cond }}
            {% endif %}
          weather_hourly_temperature_1: >
            {{ state_attr('sensor.weather_forecast_hourly', 'forecast')[1].temperature | round }}
          weather_hourly_timestamp_1: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[1].datetime) | timestamp_custom('%H:00') }}
          weather_hourly_condition_2: >
            {% set cond = state_attr('sensor.weather_forecast_hourly', '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 cond_time = as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[2].datetime) %}
            {% if ((cond_time > next_setting and cond_time < next_rising) or (cond_time < next_setting and cond_time < next_rising and next_rising < next_setting)) %}
                {% if cond == 'sunny' %} night {% elif cond == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond }} {% endif %}
            {% else %}
                {{ cond }}
            {% endif %}
          weather_hourly_temperature_2: >
            {{ state_attr('sensor.weather_forecast_hourly', 'forecast')[2].temperature | round }}
          weather_hourly_timestamp_2: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[2].datetime) | timestamp_custom('%H:00') }}
          weather_hourly_condition_3: >
            {% set cond = state_attr('sensor.weather_forecast_hourly', 'forecast')[3].condition %}
            {% set next_setting = as_timestamp(state_attr('sun.sun', 'next_setting')) %}
            {% set next_rising = as_timestamp(state_attr('sun.sun', 'next_rising')) %}
            {% set cond_time = as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[3].datetime) %}
            {% if ((cond_time > next_setting and cond_time < next_rising) or (cond_time < next_setting and cond_time < next_rising and next_rising < next_setting)) %}
                {% if cond == 'sunny' %} night {% elif cond == 'partlycloudy' %} night-partly-cloudy {% else %} {{ cond }} {% endif %}
            {% else %}
                {{ cond }}
            {% endif %}
          weather_hourly_temperature_3: >
            {{ state_attr('sensor.weather_forecast_hourly', 'forecast')[3].temperature | round }}
          weather_hourly_timestamp_3: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_hourly', 'forecast')[3].datetime) | timestamp_custom('%H:00') }}
          # Daily temperature + condition + day of week (3x)
          weather_daily_condition_0: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[0].condition }}
          weather_daily_temperature_0: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[0].temperature | round }}
          weather_daily_templow_0: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[0].templow | round }}
          weather_daily_timestamp_0: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_daily', 'forecast')[0].datetime) | timestamp_custom('%a') }}
          weather_daily_condition_1: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[1].condition }}
          weather_daily_temperature_1: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[1].temperature | round }}
          weather_daily_templow_1: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[1].templow | round }}
          weather_daily_timestamp_1: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_daily', 'forecast')[1].datetime) | timestamp_custom('%a') }}
          weather_daily_condition_2: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[2].condition }}
          weather_daily_temperature_2: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[2].temperature | round }}
          weather_daily_templow_2: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[2].templow | round }}
          weather_daily_timestamp_2: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_daily', 'forecast')[2].datetime) | timestamp_custom('%a') }}
          weather_daily_condition_3: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[3].condition }}
          weather_daily_temperature_3: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[3].temperature | round }}
          weather_daily_templow_3: >
            {{ state_attr('sensor.weather_forecast_daily', 'forecast')[3].templow | round }}
          weather_daily_timestamp_3: >
            {{ as_timestamp(state_attr('sensor.weather_forecast_daily', 'forecast')[3].datetime) | timestamp_custom('%a') }}

In the code above I use the entity weather.openweathermap_hourly for the current and hourly forecast and the entity weather.openweathermap for the daily forecast.

Once these have been added to Home Assistant it requires a restart to make them visible and populated. If everything has gone as expected you should now have a single entity called sensor.weatherframe_data like the example below:

weather-frame-ha-entity

This entity will be used by EMSHome to create the actual display. Making sure this is showing as expected will make the next step easier.

Program ESP32 with ESPHome

I won't go into detail of how to program the ESP32 via ESPHome since there are already lots of tutorials about it, and it can also be found on the ESPHome website how to do it via Home Assistant.

Once you have the ESP32 Driver Board connected to ESPHome it is time to configure it with the required code.

Go into ESPHome and edit the device just added:

In the editor add the following section as part of the ESPHome configuration:

# ESPHome v2022.11.2 doesn't have the support for 7.5inv3 yet, so using external driver
external_components:
  - source:
      type: git
      url: https://github.com/X4LD1M0/esphome
      ref: waveshare-color
    components: [ waveshare_epaper ]
    refresh: 0s

# Global variables for detecting if the display needs to be refreshed.
globals:
  - id: data_updated
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: initial_data_received
    type: bool
    restore_value: no
    initial_value: 'false'

# Check whether the display needs to be refreshed every minute,
# based on whether new data is received or motion is detected.
time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      - seconds: 0
        minutes: /15 # How often to check, defaults to every 15 min
        then:
          - if:
              condition:
                lambda: 'return id(data_updated) == true;'
              then:
                - lambda: 'id(initial_data_received) = true;'
                - if:
                    condition:
                      binary_sensor.is_on: motion_detected
                    then:
                      - logger.log: "Sensor data updated and activity in home detected: Refreshing display..."
                      - component.update: eink_display
                      - lambda: 'id(data_updated) = false;'
                    else:
                      - logger.log: "Sensor data updated but no activity in home - skipping display refresh."
              else:
                - logger.log: "No sensors updated - skipping display refresh."

# Include custom fonts
font:
  - file: 'fonts/GothamRnd-Book.ttf'
    id: font_small_book
    size: 18
  - file: 'fonts/GothamRnd-Book.ttf'
    id: font_medium_book
    size: 30
    glyphs: ['M', 'T', 'W', 'F', 'S', 'o', 'n', 'u', 'e', 'd', 'h', 'r', 'i', 'a', 't',':','0','1','2','3','4','5','6','7','8','9']
  - 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']
  - file: 'fonts/GothamRnd-Bold.ttf'
    id: font_medium_bold
    size: 30
    glyphs: ['-', ' ', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', 'E', 'H', 'K', 'L', 'N', 'O', 'R', 'T', 'U', 'Y']
  - file: 'fonts/GothamRnd-Bold.ttf'
    id: font_small_bold
    size: 18
    glyphs: [':', '-', ' ', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C']


  # 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
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_mdi_status
    size: 18
    glyphs: &mdi-status-glyphs
      - "\U000F092F" # mdi:wifi-strength-0
      - "\U000F091F" # mdi:wifi-strength-1
      - "\U000F0922" # mdi:wifi-strength-2
      - "\U000F0925" # mdi:wifi-strength-3
      - "\U000F0928" # mdi:wifi-strength-4
      - "\U000F092E" # mdi:wifi-strength-off
      - "\U000F02DC" # mdi-home
      - "\U000F087B" # mdi-home-alert
      - "\U000F12A3" # mdi-battery-high
      - "\U000F12A2" # mdi-battery-medium
      - "\U000F12A1" # mdi-battery-low
      - "\U000F125E" # mdi-battery-off-outline
      - "\U000F0954" # mdi-clock
      - "\U000F1865" # mdi-clock-remove

binary_sensor:
  # Check if ESPHome is connected
  - platform: status
    id: connection_status
    on_state:
      then:
         - lambda: 'id(data_updated) = true;'
  # Check if motion is detected in the living room.
  - platform: homeassistant
    entity_id: binary_sensor.weatherframe_motion_detected
    id: motion_detected
    on_state:
      then:
         - lambda: 'id(data_updated) = true;'
#  - platform: ble_presence
#    ibeacon_uuid: 'a6e00976-7c13-461d-8fd3-b909ecdd88f7'
#    name: "ESP32 LRM Pixel-6"

# Call Weather sensors from HA.
sensor:
#  - platform: custom
#    lambda: |-
#      auto BQ27441 = new My_BQ27441();
#      App.register_component(BQ27441);
#      return {BQ27441};
#    sensors:
#      name: "battery level"
#      force_update: true

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

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

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

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

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

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

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

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

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

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

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

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

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

  - platform: wifi_signal
    name: "Weather-Frame-01 WiFi Signal"
    id: wifisignal
    update_interval: 60s


text_sensor:
  - platform: homeassistant
    entity_id: sensor.weatherframe_data
    attribute: weather_now_condition
    id: weather_now_condition
    on_value:
      then:
         - lambda: 'id(data_updated) = true;'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  - platform: homeassistant
    entity_id: sensor.weatherframe_data
    attribute: weather_daily_timestamp_3
    id: weather_daily_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_RED
    red: 100%
    green: 0%
    blue: 0%
    white: 0%

# 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: 20ms
    model: 7.50in-bv2
#    model: 7.50inv2b
    update_interval: never
    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_ON);
      // Show loading screen before data is received.
      if (id(initial_data_received) == false) {
        it.printf(255, 390, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "WAITING FOR DATA...");
      } else {
        // Weather Current + Hourly
        if(wifi_wificomponent->is_connected()) {
          if (id(wifisignal).state >= -30)  {
            it.print(40, 60, id(font_mdi_status), COLOR_OFF, "\U000F0928");
          } else if (id(wifisignal).state < -30 && id(wifisignal).state >= -67) {
            it.print(40, 60, id(font_mdi_status), COLOR_OFF, "\U000F0925");
          } else if (id(wifisignal).state < -67 && id(wifisignal).state >= -70) {
            it.print(40, 60, id(font_mdi_status), COLOR_OFF, "\U000F0922");
          } else if (id(wifisignal).state < -70 && id(wifisignal).state >= -80) {
            it.print(40, 60, id(font_mdi_status), COLOR_OFF, "\U000F091F");
          } else {
            it.print(40, 60, id(font_mdi_status), COLOR_OFF, "\U000F092F");
          } 
        } else {
            it.print(40, 60, id(font_mdi_status), COLOR_RED, "\U000F092E");
        }
        if(id(connection_status).state) {
            it.print(40, 80, id(font_mdi_status), COLOR_OFF, "\U000F02DC");
        } else {
            it.print(40, 80, id(font_mdi_status), COLOR_RED, "\U000F087B");
        }
        if(id(homeassistant_time).now().is_valid()) {
            it.print(450, 80, id(font_mdi_status), COLOR_OFF, "\U000F0954");
        } else {
            it.print(450, 80, id(font_mdi_status), COLOR_RED, "\U000F1865");
        }

        it.printf(255, 74, id(font_title), COLOR_OFF, TextAlign::TOP_CENTER, "WEATHER");
        it.printf(255, 148, id(font_medium_bold), COLOR_RED, TextAlign::TOP_CENTER, "CURRENT");
        it.printf(115, 198, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_now_condition).state.c_str()].c_str());
        if (id(weather_now_temperature).state <= 0) {
          it.printf(331, 204, id(font_large_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_now_temperature).state);
          it.printf(328, 201, id(font_large_bold), COLOR_ON, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_now_temperature).state);
          it.printf(325, 198, id(font_large_bold), COLOR_RED, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_now_temperature).state);
        } else {
          it.printf(325, 198, id(font_large_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_now_temperature).state);
        }
        it.printf(255, 318, id(font_medium_bold), COLOR_RED, TextAlign::TOP_CENTER, "HOURLY");
        it.printf(120, 372, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_0).state.c_str());
        it.printf(120, 396, id(font_mdi_medium), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_0).state.c_str()].c_str());
        it.printf(120, 440, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_0).state);
        it.printf(210, 372, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_1).state.c_str());
        it.printf(210, 396, id(font_mdi_medium), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_1).state.c_str()].c_str());
        it.printf(210, 440, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_1).state);
        it.printf(300, 372, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_2).state.c_str());
        it.printf(300, 396, id(font_mdi_medium), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_2).state.c_str()].c_str());
        it.printf(300, 440, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_2).state);
        it.printf(390, 372, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_3).state.c_str());
        it.printf(390, 396, id(font_mdi_medium), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_3).state.c_str()].c_str());
        it.printf(390, 440, id(font_small_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_3).state);
        //it.printf(135, 372, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_0).state.c_str());
        //it.printf(135, 402, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_0).state.c_str()].c_str());
        //it.printf(135, 498, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_0).state);
        //it.printf(255, 372, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_1).state.c_str());
        //it.printf(255, 402, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_1).state.c_str()].c_str());
        //it.printf(255, 378, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_1).state);
        //it.printf(375, 372, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_hourly_timestamp_2).state.c_str());
        //it.printf(375, 402, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_hourly_condition_2).state.c_str()].c_str());
        //it.printf(375, 498, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_hourly_temperature_2).state);
        // Weather Forecast
        it.printf(255, 504, id(font_medium_bold), COLOR_RED, TextAlign::TOP_CENTER, "OUTLOOK");
        if (id(homeassistant_time).now().hour < 12) {
          it.printf(135, 552, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_daily_timestamp_0).state.c_str());
          it.printf(135, 582, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_daily_condition_0).state.c_str()].c_str());
          it.printf(135, 678, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_daily_temperature_0).state);
          it.printf(255, 552, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_daily_timestamp_1).state.c_str());
          it.printf(255, 582, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_daily_condition_1).state.c_str()].c_str());
          it.printf(255, 678, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_daily_temperature_1).state);
          it.printf(375, 552, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_daily_timestamp_2).state.c_str());
          it.printf(375, 582, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_daily_condition_2).state.c_str()].c_str());
          it.printf(375, 678, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_daily_temperature_2).state);
        } else {
          it.printf(135, 552, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_daily_timestamp_1).state.c_str());
          it.printf(135, 582, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_daily_condition_1).state.c_str()].c_str());
          it.printf(135, 678, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_daily_temperature_1).state);
          it.printf(255, 552, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_daily_timestamp_2).state.c_str());
          it.printf(255, 582, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_daily_condition_2).state.c_str()].c_str());
          it.printf(255, 678, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_daily_temperature_2).state);
          it.printf(375, 552, id(font_medium_book), COLOR_OFF, TextAlign::TOP_CENTER, "%s", id(weather_daily_timestamp_3).state.c_str());
          it.printf(375, 582, id(font_mdi_large), COLOR_OFF, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_daily_condition_3).state.c_str()].c_str());
          it.printf(375, 678, id(font_medium_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_daily_temperature_3).state);
        }
        it.printf(40, 720, id(font_small_book), COLOR_OFF, TextAlign::TOP_LEFT, "%4d-%02d-%02d %02d:%02d", id(homeassistant_time).now().year, id(homeassistant_time).now().month, id(homeassistant_time).now().day_of_month, id(homeassistant_time).now().hour, id(homeassistant_time).now().minute);
      }

Stephan Wijman • 31 Articles

View Articles