Great project! I built a weather and tide display using it, really pleased with how it came out! I used an HC-SR501 PIR sensor mounted in a 3D printed case to detect motion near the display and reduce refreshes.
Here’s the code for anyone who wants to do something similar.
esphome:
name: tidedisplay
friendly_name: TideDisplay
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:
encryption:
key: ""
ota:
password: ""
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Tidedisplay Fallback Hotspot"
password: "x5AGryOQrAfq"
captive_portal:
# Import JSON library
json:
# 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:
- if:
condition:
lambda: 'return id(data_updated) == true;'
then:
# - script.execute: update_screen
- 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."
# Include custom fonts
font:
- file: "fonts/GothamRnd-Bold.ttf"
id: font_xtra_large
size: 80
glyphs:
[ '-', '.','°', '0','1', '2', '3', '4', '5', '6', '7', '8', '9']
- file: 'fonts/GothamRnd-Bold.ttf'
id: font_large
size: 54
glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '/']
- file: 'fonts/GothamRnd-Bold.ttf'
id: font_medium
size: 30
glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '/']
- file: 'fonts/GothamRnd-Bold.ttf'
id: font_small
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: 80
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-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
- "\U000F0F38" # mdi-weather-tornado
- "\U000F059D" # mdi-weather-windy
- "\U000F059E" # mdi-weather-windy-variant
- file: "fonts/materialdesignicons-webfont.ttf"
id: font_icons_medium
size: 36
glyphs:
- "\U000F10C2" # Temperature High
- "\U000F10C3" # Temperature Low
- "\U000F050F" # mdi-thermometer
- "\U000F029A" # mdi-gauge
- "\U000F058E" # mdi-water-percent
- "\U000F07E4" # mdi-molecule-co2
- "\U000F059D" # mdi-weather-windy
- "\U000F0140" # mdi-chevron-down
- "\U000F0143" # mdi-chevron-up
- "\U000F078D" # mdi-waves
- "\U000F059B" # mdi-sunset
- "\U000F059C" # mdi-sunrise
- file: "fonts/materialdesignicons-webfont.ttf"
id: font_icons_small
size: 18
glyphs:
- "\U000F059D" # mdi-weather-windy
binary_sensor:
- platform: gpio
pin: GPIO32
name: "PIR Sensor"
id: motion_detected
device_class: motion
sensor:
# Create sensors for monitoring Weatherman remotely.
- platform: template
name: "Tide Display - Display Last Update"
device_class: timestamp
entity_category: "diagnostic"
id: display_last_update
- platform: template
name: "Tide Display - 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: "Tide Display - WiFi Signal Strength"
id: wifisignal
unit_of_measurement: "dBm"
entity_category: "diagnostic"
update_interval: 60s
- platform: homeassistant
entity_id: weather.forecast_home
attribute: temperature
id: weather_temperature_now
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: weather.forecast_home
attribute: humidity
id: weather_humidity_now
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: weather.forecast_home
attribute: pressure
id: weather_pressure_now
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: weather.forecast_home
attribute: wind_speed
id: weather_wind_speed_now
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: weather.forecast_home
attribute: wind_bearing
id: weather_wind_bearing_now
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: sensor.holyhead_tide_next_high_tide_height
id: tide_next_high_height
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: sensor.holyhead_tide_next_low_tide_height
id: tide_next_low_height
on_value:
then:
- lambda: 'id(data_updated) = true;'
text_sensor:
- platform: homeassistant
entity_id: weather.forecast_home
id: weather_condition_now
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: weather.forecast_home
attribute: forecast
id: weather_forecast_5
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: sensor.holyhead_tide_next_high_tide_time
id: tide_next_high_time
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: sensor.holyhead_tide_next_low_tide_time
id: tide_next_low_time
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: sensor.sun_next_rising_time
id: sun_next_rising
on_value:
then:
- lambda: 'id(data_updated) = true;'
- platform: homeassistant
entity_id: sensor.sun_next_setting_time
id: sun_next_setting
on_value:
then:
- lambda: 'id(data_updated) = true;'
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.50inV2
update_interval: never
rotation: 90°
lambda: |-
#define xres 480
#define yres 800
#define x_pad 10 // border padding
#define y_pad 60 // border padding
#define val_pad 70 // padding before value
#define icon_y_pad 8 //padding after icons
#define y_header_weather 40
#define y_header_forecast 290
#define y_header_tides 570
int y = 10;
char str[17];
time_t Time;
auto wind_dir = [](float dir)
{
if (11.25 < dir && dir <= 33.75) return "NNE";
else if (33.75 < dir && dir <= 56.25) return "NE";
else if (56.25 < dir && dir <= 78.75) return "ENE";
else if (78.75 < dir && dir <= 101.25) return "E";
else if (101.25 < dir && dir <= 123.75) return "ESE";
else if (123.75 < dir && dir <= 146.25) return "SE";
else if (146.25 < dir && dir <= 168.75) return "SSE";
else if (168.75 < dir && dir <= 191.25) return "S";
else if (191.25 < dir && dir <= 213.75) return "SSW";
else if (213.75 < dir && dir <= 236.25) return "SW";
else if (236.25 < dir && dir <= 258.75) return "WSW";
else if (258.75 < dir && dir <= 281.25) return "W";
else if (281.25 < dir && dir <= 303.75) return "WNW";
else if (303.75 < dir && dir <= 326.25) return "NW";
else if (326.25 < dir && dir <= 348.75) return "NNW";
else return "N";
};
static std::map<int, const char *> DoW {
{ 0, "Sunday"},
{ 1, "Monday"},
{ 2, "Tuesday"},
{ 3, "Wednesday"},
{ 4, "Thursday"},
{ 5, "Friday"},
{ 6, "Saturday"},
};
auto weather_icon = [](std::string condition) {
// Map weather states to MDI characters.
static std::map<std::string, std::string> weather_icon_map
{
{"clear-night", "\U000F0594"},
{"cloudy", "\U000F0590"},
{"cloudy-alert", "\U000F0F2F"},
{"cloudy-arrow-right", "\U000F0E6E"},
{"fog", "\U000F0591"},
{"hail", "\U000F0592"},
{"hazy", "\U000F0F30"},
{"hurricane", "\U000F0898"},
{"lightning", "\U000F0593"},
{"lightning-rainy", "\U000F067E"},
{"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"},
{"tornado", "\U000F0F38"},
{"windy", "\U000F059D"},
{"windy-variant", "\U000F059E"},
};
return weather_icon_map[condition].c_str();
};
// Show loading screen before data is received.
if (id(initial_data_received) == false) {
it.printf(240, 390, id(font_medium), color_text, TextAlign::TOP_CENTER, "WAITING FOR DATA...");
} else {
// Current Weather Section
it.printf(x_pad, y_header_weather, id(font_medium), TextAlign::BASELINE_LEFT, "Current Conditions");
it.line(x_pad, y_header_weather+5, xres-x_pad, y_header_weather+5);
// Temperature
if(id(weather_temperature_now).has_state())
it.printf(xres - x_pad, y_header_weather+70, id(font_large), TextAlign::BASELINE_RIGHT, "%2.1f°C", id(weather_temperature_now).state);
// Humidity
it.printf(x_pad, y_header_weather+60, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F058E");
if(id(weather_humidity_now).has_state())
it.printf(x_pad+40, y_header_weather+60, id(font_medium), TextAlign::BASELINE_LEFT, "%2.0f%%", id(weather_humidity_now).state);
// Pressure
it.printf(x_pad, y_header_weather+105, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F029A");
if(id(weather_pressure_now).has_state())
it.printf(x_pad+40, y_header_weather+105, id(font_medium), TextAlign::BASELINE_LEFT, "%4.0f hPa", id(weather_pressure_now).state);
// Wind
it.printf(x_pad, y_header_weather+150, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F059D");
if(id(weather_wind_speed_now).has_state() && id(weather_wind_bearing_now).has_state())
it.printf(x_pad+40, y_header_weather+150, id(font_medium), TextAlign::BASELINE_LEFT, "%2.1f km/h %s", id(weather_wind_speed_now).state, wind_dir(id(weather_wind_bearing_now).state));
// Icon
if(id(weather_condition_now).has_state())
it.printf(xres-x_pad, y_header_weather+150, id(font_mdi_large), TextAlign::BASELINE_RIGHT, "%s", weather_icon(id(weather_condition_now).state));
// Print sunrise and sunset times
it.printf(x_pad, y_header_weather+195, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F059C");
if(id(sun_next_rising).has_state())
it.printf(x_pad + 40, y_header_weather+195, id(font_medium), TextAlign::BASELINE_LEFT, "%s", id(sun_next_rising).state.c_str());
it.printf(x_pad + 160, y_header_weather+195, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F059B");
if(id(sun_next_setting).has_state ())
it.printf(x_pad + 160 + 40, y_header_weather+195, id(font_medium), TextAlign::BASELINE_LEFT, "%s", id(sun_next_setting).state.c_str());
// Forecast Weather Section
it.printf(x_pad, y_header_forecast, id(font_medium), TextAlign::BASELINE_LEFT, "Forecast");
it.line(x_pad, y_header_forecast+5, xres-x_pad, y_header_forecast+5);
if (id(weather_forecast_5).has_state()) {
int wxx = x_pad - 5;
int wyy = y_header_forecast + 30;
int forday = id(homeassistant_time).now().day_of_week; // HA DoW is 1 based. Array is 0 based so this is "tomorrow"
DynamicJsonDocument doc(2048);
deserializeJson(doc, (id(weather_forecast_5).state.c_str()));
JsonArray root = doc.as<JsonArray>();
for (int i = 0; i < 3; ++i) {
JsonObject root_x = root[i];
std::string root_0_condition = root_x["condition"];
float root_0_temperature = root_x["temperature"];
float root_0_wind_speed = root_x["wind_speed"];
float root_0_wind_bearing = root_x["wind_bearing"];
it.printf(wxx + 70, wyy, id(font_small), TextAlign::TOP_CENTER, "%s", DoW[(forday + i) % 7]);
it.printf(wxx + 70, wyy + 30, id(font_mdi_large), TextAlign::TOP_CENTER, "%s", weather_icon(root_0_condition));
it.printf(wxx + 70, wyy + 120, id(font_medium), TextAlign::TOP_CENTER, "%.0f°C", root_0_temperature);
it.printf(wxx, wyy+180, id(font_icons_small), TextAlign::BASELINE_LEFT, "\U000F059D");
it.printf(wxx+20, wyy+180, id(font_small), TextAlign::BASELINE_LEFT, "%.0f km/h %s", root_0_wind_speed, wind_dir(root_0_wind_bearing));
wxx += 160;
}
}
// Tides Section
it.printf(x_pad, y_header_tides, id(font_medium), TextAlign::BASELINE_LEFT, "Next Tides");
it.line(x_pad, y_header_tides+5, xres-x_pad, y_header_tides+5);
it.printf(x_pad, y_header_tides+60, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F0143");
if(id(tide_next_high_time).has_state() && id(tide_next_high_height).has_state())
it.printf(x_pad + 40, y_header_tides+60, id(font_medium), TextAlign::BASELINE_LEFT, "High %s (%.2fm)", id(tide_next_high_time).state.c_str(), id(tide_next_high_height).state);
it.printf(x_pad, y_header_tides+105, id(font_icons_medium), TextAlign::BASELINE_LEFT, "\U000F0140");
if(id(tide_next_low_time).has_state() && id(tide_next_low_height).has_state())
it.printf(x_pad + 40, y_header_tides+105, id(font_medium), TextAlign::BASELINE_LEFT, "Low %s (%.2fm)", id(tide_next_low_time).state.c_str(), id(tide_next_low_height).state);
// Refresh Timestamp
Time = id(homeassistant_time).now().timestamp;
strftime(str, sizeof(str), "%H:%M %x", localtime(&Time));
it.printf(240, yres-80, id(font_small), color_text, TextAlign::TOP_CENTER, "Refreshed at %s", str);
}