*** I’VE UPDATED MY POST WITH MY LATEST CODE ***
I’m no more using DarkSky as it will be removed soon.
Had to show my latest project as asked here: E-paper display
Parts are (total is about 50€): a 4.2" E-Ink display from Waveshare, a Lolin D32, a SDS011 (Air Quality), a BME280 (Temp, Humidity and Pressure), a buzzer and a LED (for notification):
Software used is ESPHome (what a great project!)
Nearly no soldering (only BME280 if I remember correctly)! This ESP32 has all the GPIO + all 3V3 and 5V needed.
My goal was to make a clone of the Xiaomi Mi Clear Grass Air Detector and to replace my Oregon weather station but where I am able to display what I want. I still need to make a case
Here is my code
substitutions:
devicename: AQI Display
gpio_led_status: GPIO5
gpio_i2c_sda: GPIO21
gpio_i2c_scl: GPIO22
gpio_uart_rx_pin: GPIO14
gpio_uart_tx_pin: GPIO13
gpio_spi_clk_pin: GPIO25
gpio_spi_mosi_pin: GPIO26
gpio_cs_pin: GPIO32
gpio_busy_pin: GPIO33
gpio_reset_pin: GPIO27
gpio_dc_pin: GPIO17
gpio_buzzer: GPIO04
gpio_led_red: GPIO19
gpio_led_green: GPIO23
gpio_led_blue: GPIO18
esphome:
name: aqi-display
comment: "Air Quality Display"
platform: ESP32
board: lolin_d32
status_led:
pin:
number: $gpio_led_status
inverted: True
i2c:
sda: $gpio_i2c_sda
scl: $gpio_i2c_scl
scan: False
id: bme_280
uart:
rx_pin: $gpio_uart_rx_pin
tx_pin: $gpio_uart_tx_pin
baud_rate: 9600
id: sds_011
spi:
clk_pin: $gpio_spi_clk_pin
mosi_pin: $gpio_spi_mosi_pin
id: epaper_display
binary_sensor:
- platform: status
name: "$devicename Status"
- platform: homeassistant
name: "Weather Alert"
entity_id: binary_sensor.alerte_meteo
id: weather_alert
internal: true
- platform: homeassistant
name: "Out Temp Rising"
entity_id: binary_sensor.jardin_temp_rising
id: out_temp_rising
internal: true
- platform: homeassistant
name: "Out Temp Failing"
entity_id: binary_sensor.jardin_temp_falling
id: out_temp_failing
internal: true
sensor:
- platform: uptime
name: "$devicename Uptime Sec"
id: uptime_sec
internal: true
- platform: wifi_signal
name: "$devicename WiFi Signal"
update_interval: 120s
- platform: template
id: uptime_timestamp
name: "$devicename Uptime"
device_class: "timestamp"
accuracy_decimals: 0
update_interval: never
lambda: |-
static float timestamp = (
id(current_time).utcnow().timestamp - id(uptime_sec).state
);
return timestamp;
- platform: bme280
i2c_id: bme_280
temperature:
name: "$devicename Temperature"
id: bme280_temp
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 5
humidity:
name: "$devicename Humidity"
id: bme280_hum
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 5
pressure:
name: "$devicename Pressure"
id: bme280_pressure
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 5
- lambda: |-
const float STANDARD_ALTITUDE = 160; // in meters
return x / powf(1 - ((0.0065 * STANDARD_ALTITUDE) /
(id(bme280_temp).state + (0.0065 * STANDARD_ALTITUDE) + 273.15)), 5.257); // in hPa
address: 0x76
update_interval: 60s
- platform: sds011
pm_2_5:
name: "$devicename PM <2.5µm Concentration"
id: pm_2_5
pm_10_0:
name: "$devicename PM <10.0µm Concentration"
id: pm_10
update_interval: 10min
- platform: homeassistant
name: "1st floor Temp. from HA"
entity_id: sensor.rdc_temperature
unit_of_measurement: "°"
accuracy_decimals: 1
id: average_1st_temp
internal: true
- platform: homeassistant
name: "1st floor Hum. from HA"
entity_id: sensor.rdc_humidity
unit_of_measurement: "%"
accuracy_decimals: 0
id: average_1st_hum
internal: true
- platform: homeassistant
name: "2nd floor Temp. from HA"
entity_id: sensor.1er_temperature
unit_of_measurement: "°"
accuracy_decimals: 1
id: average_2nd_temp
internal: true
- platform: homeassistant
name: "2nd floor Hum. from HA"
entity_id: sensor.1er_humidity
unit_of_measurement: "%"
accuracy_decimals: 0
id: average_2nd_hum
internal: true
- platform: homeassistant
name: "Outdoor Temp. from HA"
entity_id: sensor.jardin_th_temperature
unit_of_measurement: "°"
accuracy_decimals: 1
id: outdoor_temp
internal: true
- platform: homeassistant
name: "Outdoor Hum. from HA"
entity_id: sensor.jardin_th_humidity
unit_of_measurement: "%"
accuracy_decimals: 0
id: outdoor_hum
internal: true
- platform: template
name: "$devicename Air Quality Index"
lambda: |-
float value_temp = id(bme280_temp).state;
float index_temp = 5;
if (value_temp <= 14 || value_temp >= 25) {
index_temp = 1;
} else if (value_temp <= 15 || value_temp >= 24) {
index_temp = 2;
} else if (value_temp <= 16 || value_temp >= 23) {
index_temp = 3;
} else if (value_temp < 18 || value_temp > 21) {
index_temp = 4;
}
float value_hum = id(bme280_hum).state;
float index_hum = 5;
if (value_hum < 10 || value_hum > 90) {
index_hum = 1;
} else if (value_hum < 20 || value_hum > 80) {
index_hum = 2;
} else if (value_hum < 30 || value_hum > 70) {
index_hum = 3;
} else if (value_hum < 40 || value_hum > 60) {
index_hum = 4;
}
float value_pm2_5 = id(pm_2_5).state;
float index_pm2_5 = 5;
if (value_pm2_5 > 64) {
index_pm2_5 = 1;
} else if (value_pm2_5 > 53) {
index_pm2_5 = 2;
} else if (value_pm2_5 > 41) {
index_pm2_5 = 3;
} else if (value_pm2_5 > 23) {
index_pm2_5 = 4;
}
float value_pm10 = id(pm_10).state;
float index_pm10 = 5;
if (value_pm10 > 64) {
index_pm10 = 1;
} else if (value_pm10 > 53) {
index_pm10 = 2;
} else if (value_pm10 > 41) {
index_pm10 = 3;
} else if (value_pm10 > 23) {
index_pm10 = 4;
}
return ((index_temp + index_hum + index_pm2_5 + index_pm10) * 13) / 4;
unit_of_measurement: "%"
icon: "mdi:air-filter"
update_interval: 60s
id: aqi_index
on_value_range:
- below: 38.0
then:
- light.turn_on:
id: led
brightness: 100%
red: 100%
green: 0
blue: 0
- above: 39
then:
- light.turn_off:
id: led
switch:
- platform: restart
name: "$devicename Restart"
- platform: gpio
pin: $gpio_buzzer
name: "$devicename Buzzer"
icon: "mdi:volume-high"
id: buzzer
text_sensor:
- platform: version
name: "$devicename Version"
hide_timestamp: true
- platform: wifi_info
ip_address:
name: "$devicename IPv4"
icon: "mdi:server-network"
ssid:
name: "$devicename Connected SSID"
icon: "mdi:wifi"
- platform: homeassistant
name: "Today Weather Forecast"
entity_id: sensor.weather_forecast_0
id: forcast
internal: true
- platform: homeassistant
name: "Today Weather Icon"
entity_id: sensor.weather_condition_0
id: weather_icon
internal: true
- platform: homeassistant
name: "Tomorrow Weather Forecast"
entity_id: sensor.weather_forecast_1
id: forcast_1
internal: true
- platform: homeassistant
name: "Tomorrow Weather Icon"
entity_id: sensor.weather_condition_0
id: weather_icon_1
internal: true
- platform: template
name: "$devicename Air Quality Level"
lambda: |-
if (id(aqi_index).state <= 25) {
return {"Très Mauvais"}; // INADEQUATE
} else if (id(aqi_index).state <= 38) {
return {"Mauvais"}; // POOR
} else if (id(aqi_index).state <= 51) {
return {"Moyen"}; // FAIR
} else if (id(aqi_index).state <= 60) {
return {"Bon"}; // GOOD
} else {
return {"Très Bon"}; // EXCELLENT
}
icon: "mdi:air-filter"
update_interval: 60s
id: aqi_level
time:
- platform: homeassistant
timezone: Europe/Paris
id: current_time
on_time_sync:
- component.update: uptime_timestamp
output:
- platform: ledc
pin: $gpio_led_red
id: redgpio
- platform: ledc
pin: $gpio_led_green
id: greengpio
- platform: ledc
pin: $gpio_led_blue
id: bluegpio
light:
- platform: rgb
name: "$devicename LED"
red: redgpio
green: greengpio
blue: bluegpio
id: led
display:
- platform: waveshare_epaper
id: epaper
cs_pin: $gpio_cs_pin
busy_pin: $gpio_busy_pin
reset_pin: $gpio_reset_pin
dc_pin: $gpio_dc_pin
model: 4.20in # 300x400
rotation: 270°
update_interval: 60s
lambda: |-
ESP_LOGI("display", "Updating...");
// OUTSIDE
it.printf(7, 15, id(font_medium_20), TextAlign::BASELINE_LEFT, "Dehors");
it.line(78, 14, 293, 14);
it.printf(10, 84, id(icon_font_35), TextAlign::BASELINE_LEFT, "\U000F058E");
if (id(outdoor_hum).has_state()) {
it.printf(41, 82, id(font_regular_45), TextAlign::BASELINE_LEFT, "%2.0f", id(outdoor_hum).state);
it.printf(95, 82, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
}
it.printf(120, 80, id(icon_font_40), TextAlign::BASELINE_LEFT, "\U000F050F");
if (id(outdoor_temp).has_state()) {
it.printf(220, 82, id(font_regular_65), TextAlign::BASELINE_CENTER, "%.1f°", id(outdoor_temp).state);
}
if (id(out_temp_rising).has_state() && id(out_temp_failing).has_state()) {
if (id(out_temp_rising).state == true) {
it.printf(296, 82, id(icon_font_20), TextAlign::BASELINE_RIGHT, "\U000F005C");
}
if (id(out_temp_failing).state == true) {
it.printf(296, 82, id(icon_font_20), TextAlign::BASELINE_RIGHT, "\U000F0043");
}
}
// INSIDE
it.printf(7, 110, id(font_medium_20), TextAlign::BASELINE_LEFT, "Dedans");
it.line(81, 109, 293, 109);
// Floor 1
it.printf(18, 162, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F0D80");
it.printf(60, 161, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F058E");
if (id(average_2nd_hum).has_state()) {
it.printf(125, 160, id(font_regular_35), TextAlign::BASELINE_RIGHT, "%.0f", id(average_2nd_hum).state);
it.printf(128, 160, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
}
it.printf(160, 159, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F050F");
if (id(average_2nd_temp).has_state()) {
it.printf(290, 160, id(font_regular_45), TextAlign::BASELINE_RIGHT, "%.1f°", id(average_2nd_temp).state);
}
// Floor 0
it.printf(18, 212, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F0DD2");
it.printf(60, 211, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F058E");
if (id(average_1st_hum).has_state()) {
it.printf(125, 210, id(font_regular_35), TextAlign::BASELINE_RIGHT, "%.0f", id(average_1st_hum).state);
it.printf(128, 210, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
}
it.printf(160, 209, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F050F");
if (id(average_1st_temp).has_state()) {
it.printf(290, 210, id(font_regular_45), TextAlign::BASELINE_RIGHT, "%.1f°", id(average_1st_temp).state);
}
// AQI
it.printf(20, 255, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F0D43");
it.printf(68, 255, id(font_regular_30), TextAlign::BASELINE_LEFT, "%s", id(aqi_level).state.c_str());
// WEATHER
it.printf(7, 285, id(font_medium_20), TextAlign::BASELINE_LEFT, "Météo");
it.line(70, 284, 293, 284);
int time = id(current_time).now().hour * 100 + id(current_time).now().minute;
if (id(weather_icon).has_state()) {
std::map<std::string, std::string> weather_state {
{ "clear-night", "\U000F0594" },
{ "cloudy", "\U000F0590" },
{ "exceptional", "\U000F05D6" },
{ "fog", "\U000F0591" },
{ "hail", "\U000F0592" },
{ "lightning", "\U000F0593" },
{ "lightning-rainy", "\U000F067E" },
{ "partlycloudy", "\U000F0595"},
{ "pouring", "\U000F0596" },
{ "rainy", "\U000F0597" },
{ "snowy", "\U000F0598" },
{ "snowy-rainy", "\U000F067F" },
{ "sunny", "\U000F0599" },
{ "windy", "\U000F059D" },
{ "windy-variant", "\U000F059E" }
};
if (time < 1900) {
it.printf(20, 320, id(icon_font_25), TextAlign::BASELINE_LEFT, weather_state[id(weather_icon).state.c_str()].c_str());
} else {
it.printf(20, 320, id(icon_font_25), TextAlign::BASELINE_LEFT, weather_state[id(weather_icon_1).state.c_str()].c_str());
}
}
if (id(weather_alert).has_state() && id(weather_alert).state) {
// mdi:alert-outline
it.printf(50, 320, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F002A");
}
if (id(forcast).has_state() && time < 1900) {
it.printf(290, 321, id(font_regular_30), TextAlign::BASELINE_RIGHT, "%s", id(forcast).state.c_str());
}
else if (id(forcast_1).has_state() && time >= 1900) {
it.printf(290, 321, id(font_regular_30), TextAlign::BASELINE_RIGHT, "%s", id(forcast_1).state.c_str());
}
// TIME
it.line(7, 337, 293, 337);
it.strftime(7, 363, id(font_medium_20), TextAlign::BASELINE_LEFT, "%A", id(current_time).now());
it.strftime(7, 393, id(font_medium_20), TextAlign::BASELINE_LEFT, "%d %b. %y", id(current_time).now());
it.strftime(290, 393, id(font_regular_65), TextAlign::BASELINE_RIGHT, "%H:%M", id(current_time).now());
font:
- file: 'fonts/Kanit-Medium.ttf'
id: font_medium_20
size: 20
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/Kanit-Regular.ttf'
id: font_regular_30
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/Kanit-Regular.ttf'
id: font_regular_35
size: 35
glyphs:
['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']
- file: 'fonts/Kanit-Regular.ttf'
id: font_regular_45
size: 45
glyphs:
['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']
- file: 'fonts/Kanit-Regular.ttf'
id: font_regular_65
size: 65
glyphs:
['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_20
size: 20
glyphs: [
"\U000F0043", # mdi-arrow-bottom-right
"\U000F005C" # mdi-arrow-top-right
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_25
size: 25
glyphs: [
"\U000F058E", # mdi-water-percent
"\U000F0D43", # mdi-air-filter
"\U000F002A", # mdi-alert-outline
"\U000F0594", # mdi-weather-night - clear-night
"\U000F0590", # mdi-weather-cloudy
"\U000F05D6", # mdi-alert-circle-outline - exeptionnal
"\U000F0591", # mdi-weather-fog
"\U000F0592", # mdi-weather-hail
"\U000F0593", # mdi-weather-lightning
"\U000F067E", # mdi-weather-lightning-rainy
"\U000F0595", # mdi-weather-partly-cloudy
"\U000F0596", # mdi-weather-pouring
"\U000F0597", # mdi-weather-rainy
"\U000F0598", # mdi-weather-snowy
"\U000F067F", # mdi-weather-snowy-rainy
"\U000F0599", # mdi-weather-sunny
"\U000F059D", # mdi-weather-windy
"\U000F059E" # mdi-weather-windy-variant
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_30
size: 30
glyphs: [
"\U000F050F", # mdi-thermometer
"\U000F0D80", # mdi-home-floor-1
"\U000F0DD2" # mdi-home-floor-0
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_35
size: 35
glyphs: ["\U000F058E"] # mdi-water-percent
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font_40
size: 40
glyphs: ["\U000F050F"] # mdi-thermometer
wifi:
networks:
- ssid: !secret ssid
password: !secret password
- ssid: !secret ssid_live
password: !secret password_live
ap:
ssid: "$devicename Fallback Hotspot"
password: !secret api_ota_pwd
captive_portal:
web_server:
port: 80
auth:
username: admin
password: !secret api_ota_pwd
logger:
api:
password: !secret api_ota_pwd
ota:
password: !secret api_ota_pwd
Weather infos are from MétéoFrance integration but it should work with all weather integrations (except DarkSky as it has not been updated). Before 7PM, it display weather forecast for the day, after 7PM it display forecast for tomorrow.
AQI calculation is done directly on the ESP and is based on code from @Limych: Indoor Air Quality Sensor Component (Thanks!)