E-Ink Display

Crafting Tranquility: DIY E-Ink Dashboard with Homeassistant and ESP32

IOT Dec 10, 2023

I wanted a small dashboard, but I don't want a glossy screen that lights up in the dark and so on. So In my case, I got myself an E-Ink display and I wished for a project like this. So I decided to use the E-Ink Display for displaying my data in my living room.

The required information for me was classical the weather temperature and conditions and also the departure times from the public transport (for my son) from and to school. The final result looks like this:

Let's get started!

Preparing Homeassistant

I use homeassistant for gathering the data. The information will be fetched from multiple endpoints and written to the message bus. You can fetch it from there, but I will use the homeassistant API because ESPPome has a well-working integration for this.

I use the following integrations

Configuration for the weather integration

This configuration is very simple, you must only add a new entry to the integration and insert into the configuration your geo-coordinates:

Configuration for Deutsche Bahn

I want to display the departures from the local bus stop from my home to the school from my son, and also the way back to home also. So I must add two entries to gather the information. The configration is very simple just add a new entry and insert the start and target station. Let the duration to 0

Important! Your entries must match the given ones via the api,to get the right station names, you can query it against this page
Deutsche Bahn: bahn.de - Verbindungen - Ihre Anfrage

Summarize the Data in Homeassistant

So after the first data was fetched, we can now work with them. Instead of getting the data from different sensors, I used some new sensors that will be hosted extra for the display. So I am very flexible about getting the data from another service in the future (maybe my own weather station or so.th.). So I decided to deep dive into the creation of templates, and this was my result

....

sensor:

  - platform: template
    ## The public stransport Template
    sensors:
      current_to_school:
       friendly_name: Current Bus to schoool
       unique_id: current_to_school
       value_template: >-
        {{((as_timestamp(strptime(((now().date()| string) +" " + state_attr('sensor.bochum_birkenfeldstr_to_bochum_freiheitsstr', 'departure')  + ":00.000000+01:00"), '%Y-%m-%d %H:%M:%S.%f%z')) -as_timestamp(now()) )/ 60)| round  }}
      next_to_school:
       friendly_name: Next Bus to schoool
       unique_id: next_to_school
       value_template: >-
        {{((as_timestamp(strptime(((now().date()| string) +" " + state_attr('sensor.bochum_birkenfeldstr_to_bochum_freiheitsstr', 'next')  + ":00.000000+01:00"), '%Y-%m-%d %H:%M:%S.%f%z')) -as_timestamp(now()) )/ 60)| round  }}
      current_from_school:
       friendly_name: Current Bus from schoool
       unique_id: current_from_school
       value_template: >-
        {{((as_timestamp(strptime(((now().date()| string) +" " + state_attr('sensor.freiheitsstr_bochum_to_birkenfelstr_bochum', 'departure')  + ":00.000000+01:00"), '%Y-%m-%d %H:%M:%S.%f%z')) -as_timestamp(now()) )/ 60)| round  }}
      next_from_school:
       friendly_name: Next Bus from schoool
       unique_id: next_from_school
       value_template: >-
        {{((as_timestamp(strptime(((now().date()| string) +" " + state_attr('sensor.freiheitsstr_bochum_to_birkenfelstr_bochum', 'next')  + ":00.000000+01:00"), '%Y-%m-%d %H:%M:%S.%f%z')) -as_timestamp(now()) )/ 60)| round  }}
    ## The Weather forecast
      forecast_day_0:
        friendly_name: Today's Forecast
        unique_id: forecast_day_0
        value_template: >-
          {% set forecast = state_attr('weather.home','temperature') %}
          {% if forecast in (none, 'unavailable','unknown') %}
            0
          {% else %}
            {{ forecast }}
          {% endif %}
        attribute_templates:
          condition: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[0].get('condition',0) }}
            {% endif %}
          low: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[0].get('templow',0) }}
            {% endif %}
          day: >
            Today

      forecast_day_1:
        friendly_name: Tomorrow's Forecast
        unique_id: forecast_day_1
        value_template: >-
          {% set forecast = state_attr('weather.home','forecast') %}
          {% if forecast in (none, 'unavailable','unknown') %}
            0
          {% else %}
            {{ forecast[1].get('temperature',0) }}
          {% endif %}
        attribute_templates:
          condition: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[1].get('condition',0) }}
            {% endif %}
          low: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[1].get('templow',0) }}
            {% endif %}
          day: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              
            {% else %}
              {{ as_timestamp(forecast[1].datetime) | timestamp_custom('%a') | default('')}}
            {% endif %}

      forecast_day_2:
        friendly_name: Forecast Today Plus Two
        unique_id: forecast_day_2
        value_template: >-
          {% set forecast = state_attr('weather.home','forecast') %}
          {% if forecast in (none, 'unavailable','unknown') %}
            0
          {% else %}
            {{ forecast[2].get('temperature',0) }}
          {% endif %}
        attribute_templates:
          condition: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[2].get('condition',0) }}
            {% endif %}
          low: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[2].get('templow',0) }}
            {% endif %}
          day: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              
            {% else %}
              {{ as_timestamp(forecast[2].datetime) | timestamp_custom('%a') | default('')}}
            {% endif %}

      forecast_day_3:
        friendly_name: Forecast Today Plus Three
        unique_id: forecast_day_3
        value_template: >-
          {% set forecast = state_attr('weather.home','forecast') %}
          {% if forecast in (none, 'unavailable','unknown') %}
            0
          {% else %}
            {{ forecast[3].get('temperature',0) }}
          {% endif %}
        attribute_templates:
          condition: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[3].get('condition',0) }}
            {% endif %}
          low: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[3].get('templow',0) }}
            {% endif %}
          day: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              
            {% else %}
              {{ as_timestamp(forecast[3].datetime) | timestamp_custom('%a') | default('')}}
            {% endif %}

      forecast_day_4:
        friendly_name: Forecast Today Plus Four
        unique_id: forecast_day_4
        value_template: >-
          {% set forecast = state_attr('weather.home','forecast') %}
          {% if forecast in (none, 'unavailable','unknown') %}
            0
          {% else %}
            {{ forecast[4].get('temperature',0) }}
          {% endif %}
        attribute_templates:
          condition: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[4].get('condition',0) }}
            {% endif %}
          low: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              0
            {% else %}
              {{ forecast[4].get('templow',0) }}
            {% endif %}
          day: >
            {% set forecast = state_attr('weather.home','forecast') %}
            {% if forecast in (none, 'unavailable','unknown') %}
              
            {% else %}
              {{ as_timestamp(forecast[4].datetime) | timestamp_custom('%a') | default('')}}
            {% endif %}

      cur_time:
        friendly_name: Current Time
        unique_id: cur_time
        value_template: >
          {% set time_fmt = '%a %m/%d/%y %-I:%M %p' %}
          {{ now().strftime(time_fmt)}}
...

This will create the areas

  • Public transport information
    This will deliver the time in format HH:MM for the destination "to School" and "from School" for the next and the predeceasing schedule. This looks like this:
  • Weather data
    This will represent the actual and the weather data from the next days including the weather conditions. The Temperature is set into the state itself and the ofther relevant data are stored in the attributes of the sensors like this:
  • System informationysteminformation
    This will represent the current time so that the display can show it and we will see the last time the display was refreshed

Now while we get the weather data, you will be able to use the hardware and wire it together

The Hardware together

So you need the following hardware

  • A Picture frame, I use the one from Ikea called Ribba
  • Waveshare E-Ink Display (amazon link)
  • ESP32 (with PAper HAT) (amazon link)
    Of course, you can use the normal paper hat and use a separate ESP32 but it's more comfortable to use the integrated one. The procedures are always the same
I figured out a Bundle that contains both pieces that are a little bit cheaper here

If you buy the ESP 32 with the integrated E-Paper HAT you can easily plug the flat cable into the socket and you can go to the next topic and write the firmware. If you want to wire it with the extra paper HAT, I can give you a reference wiring diagram here (it's from the Waveshare Wiki)

PinESP32Description
VCC3V3Power input (3.3V)
GNDGNDGround
DINP14SPI MOSI pin, data input
SCLKP13SPI CLK pin, clock signal input
CSP15Chip selection, low active
DCP27Data/command, low for commands, high for data
RSTP26Reset, low active
BUSYP25Busy status output pin (means busy)
AGAIN! Please don't use the esp8266 for this, because it has not enough memory for the next steps so use the ESP32 for this project!!!

The Firmware

The Firmware was not written by myself, because it will take a long time and also I am a big fan of ESPHome.

Something about ESPHome

I use ESPHome with docker and build the firmware with this. ESPHome works internally with platformio and compiles the complete firmware with this. As source definition, the compilation uses a YAML file. So in this, we will describe our firmware, and what it will do, and the ESPHome "compilation process" will take a part of the dependencies and will generate a final firmware that will be uploaded onto the ESP32.

The Firmewaredefinition

First of all I defined Buttons:


button:
  - platform: shutdown
    name: "Shutdown"
  - platform: restart
    name: "Restart"
  - platform: template
    name: "Refresh Screen"
    entity_category: config
    on_press:
      - script.execute: update_screen

This will represent the buttons in my homeassistant integration. This will give me the possibility to shut down, refresh or restart the display remotely.

Next, I write an internal script for setting some variables and so on

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);'

In this I set the variable data_updates to false I update the display (refreshing with new data) and increment a display counter (that will be sent to homeassistant), also I set the last update timestamp to homeassistant.

Now let me explain how I get the data from the homeassistant for example with this:

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

Here I will use the sensor forecast_day_1 so actually, it looks like this

The definition now takes the sensor data and looks at the attribute "low". In this case, it will take the value 6.6. When the value changes on the server side, then the variable data_updated will be set to true.

To interact with new data and getting every time the fresh data I use an internal timer:

time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      - seconds: 0
        minutes: /1
        then:
          - if:
              condition:
                lambda: 'return id(data_updated) == true;'
              then:
                  - logger.log: "Sensor data updated and activity in home detected: Refreshing display..."
                  - script.execute: update_screen
              else:
                - logger.log: "No sensors updated - skipping display refresh."

So this will check every minute if there is any change that occurs, to check if the variable data_updated is set to true. In this case, the script "update_screen" will be executed. Then it will update the screen with fresh data.

The code to display the value will be done within the waveshare integration, especially with the lambda segment

display:
  - platform: waveshare_epaper
    id: eink_display
    cs_pin: GPIO15
    dc_pin: GPIO27
    busy_pin: GPIO25
    reset_pin: GPIO26
    reset_duration: 2ms
    model: 7.50in-bV3
    update_interval: never
    rotation: 90°
    lambda: |-
      // Map weather states to MDI characters.
      std::map<std::string, std::string> weather_icon_map
        {
........

So here are at your own. So my implementation is only an example, but I think it is a good starting point. So instead of going through each line, I will share your the GitHub link for this.

GitHub - SBajonczak/HomeDisplay: A E-ink Dashaboard driven by Homeassistant
A E-ink Dashaboard driven by Homeassistant. Contribute to SBajonczak/HomeDisplay development by creating an account on GitHub.

Just copy the YAML definition and compile it with your ESPHome (full manual here).

Closing words

In conclusion, creating a personalized E-Ink display for your living room has proven to be a rewarding project, offering real-time information at a glance. The integration with Homeassistant provides a seamless way to gather data from various sources, ensuring that your display remains up-to-date and relevant.

By utilizing E-Ink technology, you've achieved a balance between functionality and aesthetics, avoiding the drawbacks of glossy screens in dark environments. The integration of weather information and public transport schedules adds practicality to your daily routine, especially with a focus on your son's school commute.

Configuring Homeassistant for weather and public transport data retrieval has been a straightforward process, thanks to the well-working integrations and your clear instructions. The use of templates has allowed for flexibility in displaying the data, making it adaptable to future changes or additions.

The hardware setup involving the Waveshare E-Ink Display and ESP32, specifically with the integrated Paper HAT, provides a convenient and efficient solution. Your detailed explanation of the wiring and the recommendation to use ESP32 over ESP8266 ensures a smooth implementation of the project.

The ESPHome framework simplifies the firmware development process, offering a user-friendly YAML-based definition. The inclusion of buttons for remote control and an internal script for updating the display add further functionality and control to the project.

Your commitment to sharing your implementation on GitHub is commendable, providing a valuable resource for others interested in creating their own E-Ink Dashboard. Overall, your project exemplifies the synergy of technology, creativity, and practicality, resulting in a personalized and functional addition to your living space. Happy tinkering!

Tags