Issue
One may leave the house and let one or more window(s) open.
If you own a house you may imagine that this can lead to a medium disaster, depending on weather or whatever.
I introduced some automations which send a Pushover message when you leave the homezone and a window is open but this maybe reaches you when you are some dozen meters away from home (or 2 kilometres with the car). Not really perfect…
Thanksgiving
I don’t want to adorn myself with borrowed plumes. Most of the basic work I got from this post:
AZ Touch ESP example
It gives me a great start for my project and I have to say thank you.
Objective
Create something which gives you a hint right before you leave the house. Something with a small display would be nice, preferably glowing in the dark.
Prerequisites
- Every window (or door) to show need a sensor.
- If you want to use the weather display, well, you need some weather service and sensors.
- some soldering experience
- basic understanding and experience with ESP32 and/or other microcontrollers
- basic understanding and experience with ESPHome
Solution
A small display right next to the front door, so everyone who leaves the house can easily see the window status.
Hardware
- AZ-Delivery “AZ-Touch MOD 2.8 inch”
- AZ-Delivery “ESP32 NodeMCU DevKit C”
- BH1750 Light sensor (also available at AZ-Delivery but can come from wherever)
- Since I had to power the device with AC, I used a rectifier diode and an electrolytic capacitor. If you have DC then it’s not necessary, the display has a wide voltage operation range.
ESPHome code
esphome:
name: display-01
esp32:
board: nodemcu-32s
framework:
type: arduino
# Enable logging
logger:
level: INFO
logs:
component: ERROR
# Enable Home Assistant API
api:
password: ""
# play to buzzer from HA
services:
- service: play_tune
variables:
tune_str: string
then:
- rtttl.play:
rtttl: !lambda 'return tune_str;'
ota:
password: ""
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.nnn.nnn
gateway: 192.168.nnn.nnn
subnet: 255.255.255.0
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Display 01 Fallback Hotspot"
password: "whateveryoulike"
captive_portal:
time:
- platform: homeassistant
id: ha_time
i2c:
sda: GPIO33
scl: GPIO32
sensor:
- platform: bh1750
id: display_01_illuminance
name: "Display 01: Illuminance"
address: 0x23
update_interval: 2s
accuracy_decimals: 1
on_value_range:
- below: 2.0
then:
- light.turn_on:
id: backlight
brightness: 30%
- above: 2.0
below: 20.0
then:
- light.turn_on:
id: backlight
brightness: 60%
- above: 20.0
then:
- light.turn_on:
id: backlight
brightness: 100%
- platform: homeassistant
id: weather_temperature_outside
entity_id: sensor.location_outside_temperature_current
- platform: homeassistant
id: weather_3h_temperature
entity_id: sensor.weather_3h_temperature
- platform: homeassistant
id: weather_6h_temperature
entity_id: sensor.weather_6h_temperature
binary_sensor:
- platform: status
id: display_01_status
name: "Display 01: Status"
- platform: homeassistant
id: windows_bathroom_ground
entity_id: binary_sensor.location_bathroom_ground_windows
- platform: homeassistant
id: windows_kitchen
entity_id: binary_sensor.location_kitchen_windows
- platform: homeassistant
id: windows_living_room
entity_id: binary_sensor.location_living_room_windows
- platform: homeassistant
id: windows_bedroom
entity_id: binary_sensor.location_bedroom_windows
- platform: homeassistant
id: windows_dining_room
entity_id: binary_sensor.location_dining_room_windows
- platform: homeassistant
id: windows_bathroom_upstairs
entity_id: binary_sensor.location_bathroom_upstairs_windows
- platform: homeassistant
id: windows_room_upstairs_north
entity_id: binary_sensor.location_room_upstairs_north_windows
- platform: homeassistant
id: windows_room_upstairs_south
entity_id: binary_sensor.location_room_upstairs_south_windows
- platform: template
id: windows_all
lambda: |-
return
id(windows_bathroom_ground).state ||
id(windows_kitchen).state ||
id(windows_living_room).state ||
id(windows_bedroom).state ||
id(windows_dining_room).state ||
id(windows_bathroom_upstairs).state ||
id(windows_room_upstairs_north).state ||
id(windows_room_upstairs_south).state;
text_sensor:
- platform: homeassistant
id: weather
entity_id: weather.home_5_hourly
- platform: homeassistant
id: weather_3h_condition
entity_id: sensor.weather_3h_condition
- platform: homeassistant
id: weather_6h_condition
entity_id: sensor.weather_6h_condition
switch:
- platform: restart
id: display_01_restart
name: "Display 01: Restart"
output:
# backlight
- platform: ledc
id: backlight_out
pin: GPIO15
inverted: true
# buzzer
- platform: ledc
id: rtttl_out
pin: GPIO21
# component buzzer
rtttl:
output: rtttl_out
# backlight
light:
- platform: monochromatic
id: backlight
name: "Display 01: Backlight"
output: backlight_out
restore_mode: ALWAYS_ON
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
touchscreen:
platform: xpt2046
id: mtouch
cs_pin: 14
interrupt_pin: 27
update_interval: 50ms
report_interval: 1s
swap_x_y: false
threshold: 400
calibration_x_min: 272
calibration_x_max: 3807
calibration_y_min: 3887
calibration_y_max: 384
on_touch:
- display.page.show_next: screen
- script.execute: screen_reset
script:
- id: screen_reset
mode: restart
then:
- delay: 10 s
- display.page.show: page1
color:
- id: color_background
red: 100%
green: 100%
blue: 100%
- id: color_head_text
red: 0%
green: 0%
blue: 0%
- id: color_lines
red: 0%
green: 0%
blue: 60%
- id: color_text
red: 0%
green: 0%
blue: 0%
- id: color_text_alert
red_int: 255
green_int: 163
blue_int: 0
- id: color_text_okay
red: 0%
green: 70%
blue: 0%
- id: color_coming_weather
red: 0%
green: 0%
blue: 20%
font:
- file: "font/Ubuntu-Regular.ttf"
id: font_rooms
size: 18
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: "font/Ubuntu-Regular.ttf"
id: font_header
size: 24
- file: "font/Ubuntu-Regular.ttf"
id: font_header_small
size: 18
- file: "font/Ubuntu-Bold.ttf"
id: font_alert
size: 24
- file: "font/Ubuntu-Regular.ttf"
id: font_temperature
size: 80
glyphs:
['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
- file: "font/Ubuntu-Regular.ttf"
id: font_temperature_unit
size: 36
glyphs:
['°', 'C']
- file: "font/Ubuntu-Regular.ttf"
id: font_coming_weather
size: 24
glyphs:
[' ', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'C', 'u', 's', 'i', 'c', 'h', 't', ':', '°']
image:
- file: "img/alert.png"
id: image_alert
type: RGBA
- file: "img/cloud.png"
id: image_cloud
type: RGBA
- file: "img/cloud-small.png"
id: image_cloud_small
type: RGBA
- file: "img/cloud-partly.png"
id: image_cloud_partly
type: RGBA
- file: "img/cloud-partly-small.png"
id: image_cloud_partly_small
type: RGBA
- file: "img/dot-green.png"
id: image_dot_green
resize: 14x14
type: RGB24
- file: "img/dot-orange.png"
id: image_dot_orange
resize: 14x14
type: RGB24
- file: "img/okay.png"
id: image_okay
type: RGBA
- file: "img/sun.png"
id: image_sun
type: RGBA
- file: "img/sun-small.png"
id: image_sun_small
type: RGBA
- file: "img/night.png"
id: image_night
type: RGBA
- file: "img/night-small.png"
id: image_night_small
type: RGBA
- file: "img/attention.png"
id: image_attention
type: RGBA
- file: "img/attention-small.png"
id: image_attention_small
type: RGBA
- file: "img/fog.png"
id: image_fog
type: RGBA
- file: "img/fog-small.png"
id: image_fog_small
type: RGBA
- file: "img/hail.png"
id: image_hail
type: RGBA
- file: "img/hail-small.png"
id: image_hail_small
type: RGBA
- file: "img/lightning.png"
id: image_lightning
type: RGBA
- file: "img/lightning-small.png"
id: image_lightning_small
type: RGBA
- file: "img/rain.png"
id: image_rain
type: RGBA
- file: "img/rain-small.png"
id: image_rain_small
type: RGBA
- file: "img/snow.png"
id: image_snow
type: RGBA
- file: "img/snow-small.png"
id: image_snow_small
type: RGBA
- file: "img/wind.png"
id: image_wind
type: RGBA
- file: "img/wind-small.png"
id: image_wind_small
type: RGBA
display:
- platform: ili9xxx
model: ili9341
id: screen
cs_pin: GPIO5
dc_pin: GPIO4
reset_pin: GPIO22
rotation: 90°
pages:
- id: page1
lambda: |-
static bool blink = true;
// background
it.fill(id(color_background));
// header
it.filled_rectangle(5, 30, it.get_width() - 10, 2, id(color_lines));
it.print(it.get_width() / 2, 0, id(font_header), id(color_head_text), TextAlign::TOP_CENTER, "Windows");
it.strftime(it.get_width() - 7, 4, id(font_header_small), id(color_head_text), TextAlign::TOP_RIGHT, "%H:%M", id(ha_time).now());
// bath room upstairs
if(id(windows_bathroom_upstairs).state) {
it.image(16, 45, id(image_dot_orange));
it.print(40, 40, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Bath upstairs");
} else {
it.image(16, 45, id(image_dot_green));
it.print(40, 40, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Bath upstairs");
}
// upstairs north
if(id(windows_room_upstairs_north).state) {
it.image(16, 70, id(image_dot_orange));
it.print(40, 65, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Upstairs north");
} else {
it.image(16, 70, id(image_dot_green));
it.print(40, 65, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Upstairs north");
}
// upstairs south
if(id(windows_room_upstairs_south).state) {
it.image(16, 95, id(image_dot_orange));
it.print(40, 90, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Upstairs south");
} else {
it.image(16, 95, id(image_dot_green));
it.print(40, 90, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Upstairs south");
}
it.line(16, 114, 150, 114, id(color_lines));
// bathroom
if(id(windows_bathroom_ground).state) {
it.image(16, 120, id(image_dot_orange));
it.print(40, 115, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Bathroom");
} else {
it.image(16, 120, id(image_dot_green));
it.print(40, 115, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Bathroom");
}
// kitchen
if(id(windows_kitchen).state) {
it.image(16, 145, id(image_dot_orange));
it.print(40, 140, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Kitchen");
} else {
it.image(16, 145, id(image_dot_green));
it.print(40, 140, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Kitchen");
}
// living room
if(id(windows_living_room).state) {
it.image(16, 170, id(image_dot_orange));
it.print(40, 165, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Living room");
} else {
it.image(16, 170, id(image_dot_green));
it.print(40, 165, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Living room");
}
// bedroom
if(id(windows_bedroom).state) {
it.image(16, 195, id(image_dot_orange));
it.print(40, 190, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Bedroom");
} else {
it.image(16, 195, id(image_dot_green));
it.print(40, 190, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Bedroom");
}
// dining room
if(id(windows_dining_room).state) {
it.image(16, 220, id(image_dot_orange));
it.print(40, 215, id(font_rooms), id(color_text_alert), TextAlign::TOP_LEFT, "Dining room");
} else {
it.image(16, 220, id(image_dot_green));
it.print(40, 215, id(font_rooms), id(color_text_okay), TextAlign::TOP_LEFT, "Dining room");
}
// all windows
if(id(windows_all).state) {
if(blink) it.image(245, 40, id(image_alert), ImageAlign::TOP_CENTER);
blink = !blink;
it.print(245, 160, id(font_alert), id(color_text_alert), TextAlign::TOP_CENTER, "Windows");
it.print(245, 190, id(font_alert), id(color_text_alert), TextAlign::TOP_CENTER, "open");
} else {
it.image(245, 40, id(image_okay), ImageAlign::TOP_CENTER);
it.print(245, 160, id(font_alert), id(color_text_okay), TextAlign::TOP_CENTER, "Windows");
it.print(245, 190, id(font_alert), id(color_text_okay), TextAlign::TOP_CENTER, "closed");
}
- id: page2
lambda: |-
// background
it.fill(id(color_background));
// header
it.filled_rectangle(5, 30, it.get_width() - 10, 2, id(color_lines));
it.print(it.get_width() / 2, 0, id(font_header), id(color_head_text), TextAlign::TOP_CENTER, "Weather");
it.strftime(it.get_width() - 7, 4, id(font_header_small), id(color_head_text), TextAlign::TOP_RIGHT, "%H:%M", id(ha_time).now());
//it.printf(5, 4, id(font_header_small), id(color_text), TextAlign::TOP_LEFT, "%s", id(weather).state.c_str());
// temperature
it.printf(80, 100, id(font_temperature), id(color_text), TextAlign::CENTER, "%.0f", id(weather_temperature_outside).state);
it.print(135, 69, id(font_temperature_unit), id(color_text), TextAlign::TOP_LEFT, "°C");
// current weather
if(id(weather).state == "clear-night"){
it.image(190, 40, id(image_night));
} else if(id(weather).state == "cloudy"){
it.image(190, 40, id(image_cloud));
} else if(id(weather).state == "exceptional"){
it.image(190, 40, id(image_attention));
} else if(id(weather).state == "fog"){
it.image(190, 40, id(image_fog));
} else if(id(weather).state == "hail"){
it.image(190, 40, id(image_hail));
} else if(id(weather).state == "lightning"){
it.image(190, 40, id(image_lightning));
} else if(id(weather).state == "lightning-rainy"){
it.image(190, 40, id(image_lightning));
it.image(190, 40, id(image_rain));
} else if(id(weather).state == "pouring"){
it.image(190, 40, id(image_cloud));
it.image(190, 40, id(image_rain));
it.image(200, 36, id(image_rain));
} else if(id(weather).state == "rainy"){
it.image(190, 40, id(image_cloud));
it.image(190, 40, id(image_rain));
} else if(id(weather).state == "partlycloudy"){
it.image(190, 40, id(image_sun));
it.image(190, 40, id(image_cloud_partly));
} else if(id(weather).state == "snowy"){
it.image(190, 40, id(image_cloud));
it.image(190, 40, id(image_snow));
} else if(id(weather).state == "snowy-rainy"){
it.image(190, 40, id(image_cloud));
it.image(190, 40, id(image_rain));
it.image(190, 40, id(image_snow));
} else if (id(weather).state == "sunny"){
it.image(190, 40, id(image_sun));
} else if (id(weather).state == "windy"){
it.image(190, 40, id(image_wind));
} else if (id(weather).state == "windy-variant"){
it.image(190, 40, id(image_cloud));
it.image(190, 40, id(image_wind));
}
// coming weather 3h
it.print(20, 180, id(font_coming_weather), id(color_coming_weather), TextAlign::TOP_LEFT, "Outlook 3h:");
it.printf(220, 180, id(font_coming_weather), id(color_coming_weather), TextAlign::TOP_RIGHT, "%.0f °C", id(weather_3h_temperature).state);
if(id(weather_3h_condition).state == "clear-night"){
it.image(250, 180, id(image_night_small));
} else if(id(weather_3h_condition).state == "cloudy"){
it.image(250, 180, id(image_cloud_small));
} else if(id(weather_3h_condition).state == "exceptional"){
it.image(250, 180, id(image_attention_small));
} else if(id(weather_3h_condition).state == "fog"){
it.image(250, 180, id(image_fog_small));
} else if(id(weather_3h_condition).state == "hail"){
it.image(250, 180, id(image_hail_small));
} else if(id(weather_3h_condition).state == "lightning"){
it.image(250, 180, id(image_lightning_small));
} else if(id(weather_3h_condition).state == "lightning-rainy"){
it.image(250, 180, id(image_lightning_small));
it.image(250, 180, id(image_rain_small));
} else if(id(weather_3h_condition).state == "pouring"){
it.image(250, 180, id(image_cloud_small));
it.image(250, 180, id(image_rain_small));
it.image(200, 178, id(image_rain_small));
} else if(id(weather_3h_condition).state == "rainy"){
it.image(250, 180, id(image_cloud_small));
it.image(250, 180, id(image_rain_small));
} else if(id(weather_3h_condition).state == "partlycloudy"){
it.image(250, 180, id(image_sun_small));
it.image(250, 180, id(image_cloud_partly_small));
} else if(id(weather_3h_condition).state == "snowy"){
it.image(250, 180, id(image_cloud_small));
it.image(250, 180, id(image_snow_small));
} else if(id(weather_3h_condition).state == "snowy-rainy"){
it.image(250, 180, id(image_cloud_small));
it.image(250, 180, id(image_rain_small));
it.image(250, 180, id(image_snow_small));
} else if (id(weather_3h_condition).state == "sunny"){
it.image(250, 180, id(image_sun_small));
} else if (id(weather_3h_condition).state == "windy"){
it.image(250, 180, id(image_wind_small));
} else if (id(weather_3h_condition).state == "windy-variant"){
it.image(250, 180, id(image_cloud_small));
it.image(250, 180, id(image_wind_small));
}
// coming weather 6h
it.print(20, 210, id(font_coming_weather), id(color_coming_weather), TextAlign::TOP_LEFT, "Outlook 6h:");
it.printf(220, 210, id(font_coming_weather), id(color_coming_weather), TextAlign::TOP_RIGHT, "%.0f °C", id(weather_6h_temperature).state);
if(id(weather_6h_condition).state == "clear-night"){
it.image(250, 210, id(image_night_small));
} else if(id(weather_3h_condition).state == "cloudy"){
it.image(250, 210, id(image_cloud_small));
} else if(id(weather_3h_condition).state == "exceptional"){
it.image(250, 210, id(image_attention_small));
} else if(id(weather_3h_condition).state == "fog"){
it.image(250, 210, id(image_fog_small));
} else if(id(weather_3h_condition).state == "hail"){
it.image(250, 210, id(image_hail_small));
} else if(id(weather_3h_condition).state == "lightning"){
it.image(250, 210, id(image_lightning_small));
} else if(id(weather_3h_condition).state == "lightning-rainy"){
it.image(250, 210, id(image_lightning_small));
it.image(250, 210, id(image_rain_small));
} else if(id(weather_3h_condition).state == "pouring"){
it.image(250, 210, id(image_cloud_small));
it.image(250, 210, id(image_rain_small));
it.image(200, 208, id(image_rain_small));
} else if(id(weather_3h_condition).state == "rainy"){
it.image(250, 210, id(image_cloud_small));
it.image(250, 210, id(image_rain_small));
} else if(id(weather_3h_condition).state == "partlycloudy"){
it.image(250, 210, id(image_sun_small));
it.image(250, 210, id(image_cloud_partly_small));
} else if(id(weather_3h_condition).state == "snowy"){
it.image(250, 210, id(image_cloud_small));
it.image(250, 210, id(image_snow_small));
} else if(id(weather_3h_condition).state == "snowy-rainy"){
it.image(250, 210, id(image_cloud_small));
it.image(250, 210, id(image_rain_small));
it.image(250, 210, id(image_snow_small));
} else if (id(weather_3h_condition).state == "sunny"){
it.image(250, 210, id(image_sun_small));
} else if (id(weather_3h_condition).state == "windy"){
it.image(250, 210, id(image_wind_small));
} else if (id(weather_3h_condition).state == "windy-variant"){
it.image(250, 210, id(image_cloud_small));
it.image(250, 210, id(image_wind_small));
}
Additional resources
I created some additional pictures to use in this project.
You can get from here, it’s the file ‘img.zip’.
Some explanations / hints / remarks
-
I’m from Germany… you maybe find some german words instead of english… sorry, adapt it to your needs.
-
You need to adapt anyway all the names, IP settings, Wifi settings and so on.
-
The service to make some noise or tunes with the builtin buzzer is integrated, but basically I don’t use it. (But it’s fun, try feeding the service “*_play_tune” in HA with “star_wars:d=16,o=5,b=100:4e,4e,4e,8c,p,g,4e,8c,p,g,4e,4p,4b,4b,4b,8c6,p,g,4d#,8c,p,g,4e,8p”. )
-
The background light of this display can be controlled using GPIO15. In the beginning of the project I didn’t want to control the brightness. But it became clear quite fast: When you walk near the display in the night, it’s much too bright. Then I thought about using the sun integration to dim it at night, but then the display was dimmed the whole night and when someone switched on the light in floor it was way too dark. So finally: The display need to react on the environment illuminance. So I included a BH1750 sensor and used GPIO32 and GPIO33 as I²C bus. Depending on the brightness the backlight is controlled in 3 steps. I guess it can be smoother but it’s good enough for me.
-
The touchscreen needs calibration, you can follow the documentation in ESPHome for the XPT2046 platform. In my setup it’s not very important since I do not define any touch areas. Just tap anywhere on the screen and the display changes. It also switches back to the first page after some seconds.
-
Finding the right font was a bit of a challenge since most of the fonts doesn’t look good on such small displays. I made some experiments with bitmap fonts but that was not really succesfull. In the end I used Ubuntu-Regular and Ubuntu-Bold from Google Fonts. That gives a smooth look, even with small font sizes.
-
The ESP32 I used is nearly full with this application, maybe there is more possible with more memory.
-
The weather thing is a goodie and not the main functionality. But it’s nice. To make it work I created some helper sensors in Home Assistant, just to use it in the display:
template:
- sensor:
- unique_id: "weather_3h_temperature"
name: "Weather forecast 3h: Temperature"
unit_of_measurement: "°C"
state: "{{ state_attr('weather.home_5_hourly', 'forecast')[4].temperature | float | round(1, default=12) }}"
- unique_id: "weather_3h_condition"
name: "Weather forecast 3h: Conditions"
icon: mdi:weather-partly-cloudy
state: "{{ state_attr('weather.home_5_hourly', 'forecast')[4].condition | string | default('') }}"
- unique_id: "weather_6h_temperature"
name: "Weather forecast 6h: Temperature"
unit_of_measurement: "°C"
state: "{{ state_attr('weather.home_5_hourly', 'forecast')[7].temperature | float | round(1, default=12) }}"
- unique_id: "weather_6h_condition"
name: "Weather forecast 6h: Conditions"
icon: mdi:weather-partly-cloudy
state: "{{ state_attr('weather.home_5_hourly', 'forecast')[7].condition | string | default('') }}"
-
It runs now since several months without major problems. One thing is: Sometimes (in rare cases) the touch screen is not reacting anymore, I don’t know the reason. The functionality itself keeps running, just no reaction on touch events. The solution is to power off and on again.
-
For a short moment I had the idea to make this thing anyhow powered by battery… but it’s not really feasible with this hardware and setup, I guess.