Crafting Tranquility: DIY E-Ink Dashboard with Homeassistant and ESP32
Der Sascha
Posted on December 24, 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
- Weather information from the metrologic institute
- Integration from Deutsche Bahn (a public transport system API for germany)
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
](https://mobile.bahn.de/bin/mobil/query.exe/dox?ld=4393&protocol=https%3A&n=1&i=7g.03080893.1701199074&rt=1&use_realtime_filter=1&webview=&OK=&ref=blog.bajonczak.com#focus)
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)
| Pin | ESP32 | Description |
| VCC | 3V3 | Power input (3.3V) |
| GND | GND | Ground |
| DIN | P14 | SPI MOSI pin, data input |
| SCLK | P13 | SPI CLK pin, clock signal input |
| CS | P15 | Chip selection, low active |
| DC | P27 | Data/command, low for commands, high for data |
| RST | P26 | Reset, low active |
| BUSY | P25 | Busy 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.
](https://github.com/SBajonczak/HomeDisplay/tree/main?ref=blog.bajonczak.com)
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!
Posted on December 24, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.