Thanks for the video, it was definitely helpful, I’m watching a few of his videos now.
yeah I think this is the best approach, it reminds me of the saying, “The best way to eat an elephant, is one bite at a time”
Thanks for the video, it was definitely helpful, I’m watching a few of his videos now.
yeah I think this is the best approach, it reminds me of the saying, “The best way to eat an elephant, is one bite at a time”
Unfortunately some people actually plop that whole elephant on their plate and then choke on it. Internet guides arent always right/updated, cheap chinese components dont always work right or have instructions and you can make mistakes.
There’s no penalties or fines for compiling and uploading code for one component at a time. Im not kidding here, it can take 3-4x longer to troubleshoot a medium/large size project than it takes to do it right the first time.
Thanks for referring to my post and that it was helpful to you. if you want to tinker around to learn, individual components may be fine. But if you want something more complete and ready-made I used this card
I got this two-channel one because my thermostat manages a pellet insert in which the logic when switched on closes the first relay for ignition while the second relay manages the power modulation based on the desired temperature. with that card all I needed was a Dallas DS18B20 temperature sensor and a Nextion touchscreen panel. writing the firmware and managing the panel took some time because on the primary RX TX PINS the panel did not respond, therefore since I needed to connect it to the serial 2 PIN 16 and 17, and since on these PINs who manufactured the board had connected the two relays I had to make a hardware change that allowed me to get my thermostat working well. By virtue of this, if you have a simple boiler, just purchase a module with a single relay
Thanks for the tip with the ready made board, its definitely something to think about, I’m still in two minds at the moment, I already have all the components to create a relay module from a previous load of projects and this post on a Raspberry pi forum goes a long way to helping me create a module, that hopefully one day will soldered onto a custom pcb for this project.
I hope you don’t mind I liked your design for the UI so much I adapted it using the rendering engine in EspHome, as my display doesn’t have a dedicated chip for rendering like the Nextion.
here is the updated yaml:
spi:
clk_pin: 18
mosi_pin: 23
miso_pin: 19
time:
- platform: homeassistant
id: my_time
image:
- file: mdi:power
id: power
resize: 28x28
- file: mdi:fire
id: fire
resize: 28x28
- file: mdi:chevron-up
id: chevron_up
resize: 70x70
- file: mdi:chevron-down
id: chevron_down
resize: 70x70
- file: mdi:home-assistant
id: home_assistant
resize: 90x90
font:
- file: "fonts/Roboto-Regular.ttf"
id: my_font
size: 25
- file: "fonts/Roboto-Regular.ttf"
id: big_font
size: 40
- file: "fonts/Roboto-Regular.ttf"
id: little_font
size: 20
- file: "fonts/Roboto-Bold.ttf"
id: bold_font
size: 25
color:
- id: grey
hex: D1D0CE
- id: white
hex: FFFFFF
- id: dark_grey
hex: A0A0A0
- id: HA_blue
hex: 03a9f4
display:
- platform: ili9xxx
model: ili9341
dc_pin: 5
cs_pin: 33
reset_pin: 16
dimensions:
height: 240
width: 320
transform:
swap_xy: true
mirror_y: true
mirror_x: true
lambda: |-
it.fill(id(white));
it.strftime(5, 5, id(bold_font), Color::BLACK, "%H:%M", id(my_time).now());
it.printf(5, 35, id(bold_font), Color::BLACK, "Temperature:");
it.printf(110, 63, id(little_font), Color::BLACK, "%.1f°C", id(temp1).state);
it.printf(225, 70, id(big_font), Color::BLACK, "%.1f", id(my_climate).target_temperature);
it.image(235, 5, id(chevron_up), Color::BLACK);
it.image(235,110, id(chevron_down), Color::BLACK);
it.filled_rectangle(205, 180, 110, 55, id(grey));
it.filled_rectangle(90, 180, 110, 55, id(grey));
it.image(250, 182, id(fire), id(dark_grey));
it.print(240, 207, id(little_font), id(dark_grey), "Heat");
it.image(135, 182, id(power), id(dark_grey));
it.print(133, 207, id(little_font), id(dark_grey), "Off");
it.image(5, 80, id(home_assistant), id(HA_blue));
touchscreen:
platform: xpt2046
calibration_x_min: 323
calibration_x_max: 3769
calibration_y_min: 438
calibration_y_max: 3769
transform:
swap_xy: true
id: my_touchscreen
cs_pin: 32
#interrupt_pin: 21
update_interval: 50ms
threshold: 400
on_touch:
then:
- light.control:
id: back_light
brightness: 100%
- delay: 20s
- light.control:
id: back_light
brightness: 35%
binary_sensor:
- platform: touchscreen
id: temp_down
x_min: 235
x_max: 295
y_min: 110
y_max: 160
on_press:
then:
- climate.control:
id: my_climate
target_temperature: !lambda return id(my_climate).target_temperature - 0.5;
- platform: touchscreen
id: temp_up
x_min: 235
x_max: 295
y_min: 20
y_max: 70
on_press:
then:
- climate.control:
id: my_climate
target_temperature: !lambda return id(my_climate).target_temperature + 0.5;
- platform: touchscreen
id: heating_on
x_min: 210
x_max: 315
y_min: 180
y_max: 235
on_press:
then:
if:
condition:
light.is_on: back_light
then:
- climate.control:
id: my_climate
mode: HEAT
- platform: touchscreen
id: heating_off
x_min: 90
x_max: 195
y_min: 180
y_max: 235
on_press:
then:
if:
condition:
light.is_on: back_light
then:
- climate.control:
id: my_climate
mode: "OFF"
dallas:
pin: 15
sensor:
- platform: dallas
address: 0x5d03139779b91928
id: temp1
name: "Temperature sensor"
switch:
- platform: gpio
pin: 4
id: led_one
# Define a PWM output on the ESP32
output:
- platform: ledc
pin: 17
id: gpio_17_backlight_pwm
# Define a monochromatic, dimmable light for the backlight
light:
- platform: monochromatic
output: gpio_17_backlight_pwm
name: "Display Backlight"
id: back_light
restore_mode: RESTORE_DEFAULT_ON
climate:
- platform: thermostat
name: "My Thermostat"
id: my_climate
sensor: temp1
heat_deadband: 0.5
heat_overrun: 0.5
min_heating_off_time: 6s
min_heating_run_time: 6s
min_idle_time: 6s
heat_action:
- switch.turn_on: led_one
idle_action:
- switch.turn_off: led_one
and here is a pic of the end result for today
one thing I do need to work out is, how to change the colour of the buttons when their state has changed, I’m sure it will be with an If, Else statement, but that’s a job for tomorrow.
Eeww!! I like that display! Clean and crisp graphics plus its a good size and practical. I always see people using those tiny .96" displays and im over here thinking to myself, " did they actually think that theough? Thats F’ing tiny!"
Do you have a brand/model or a link for that display?
Apologies for the amazon link, but it was a generic ILI9341 TFT display 240x320 (2.8"), so still small but it was cheap and what I needed really.
Amazon is fine. I prefer to pay a little more for the peace of mind and F supporting China and aliexpress crooks.
No worries. Just was curious. Really looking forward to this
And, just where do you think that the Amazon seller gets the parts?
@ GreyLinux - I am seriously impressed. When you are finished, please share the stl files because I’ll bet others will want to copy your project.
What will the relays be switching?
Many of the same sellers sell the same stuff at aliexpress, amazon and ebay as they are all marketplaces.
You still support China (or Chinese business) the same way but on top you that you spend extra money to make bezoz happy … beside amazon stuff continues peeing in bottles
So, for my use case it will be a single relay, used to switch my heating boiler, to heating ‘ON’ and for my use the boiler doesn’t require any voltage or very low voltage to signal heating ‘ON’ also known as dry contacts. However, depending on the use case it could be potentially used to signal a cooling unit. Although I might design a 2 relay unit for people that require both.
So there has been a massive update to the project, I have the yaml where I think it works well and everything works also I spent all of yesterday soldering the entire circuit to a prototype board and surprisingly everything worked first time.
The updates to the yaml are, incorporating a settings( probably not the best description) page by tapping the cogs icon on the home page, adding an IF statement for changing the HEAT and OFF to a different colour when the other is pressed. Finally the brightness configuration was a bit clunky before and didn’t work as expected so with the help of someone on discord I have remedied this to work perfectly.
This is the updated yaml
## Wiring
## --------
## The SPI pins, MOSI, MISO, and SCK, are shared between the display
## and the touchscreen (and SD card reader).
## I did not wire TOUCH_IRQ (add update_interval: 50ms to touchscreen instead)
## The SD card pins are not connected.
##
## LCD to ESP32 (30 pin or d1 mini) wiring:
## * SCK/CLK to 18
## * MISO/DO/SDO to 19
## * MOSI/DIN/SDI to 23
## * DISP_CS to 33
## * DISP_LED to 17
## * TOUCH_CS to 32
## * DISP_DC to 21
## * DISP_RESET to 16
## * VCC to 3V3
## * GND to GND
##
## * Relay to 22
## * Dallas to 14
esphome:
name: thermostat-one
friendly_name: thermostat-one
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
level: DEBUG
logs:
component: ERROR
# 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: "Thermostat-One Fallback Hotspot"
password: "******************"
captive_portal:
spi:
clk_pin: 18
mosi_pin: 23
miso_pin: 19
time:
- platform: homeassistant
id: my_time
text_sensor:
- platform: wifi_info
ip_address:
name: IP_Address
id: IP_Address
- platform: version
name: ESPHome_Version
id: ESPHome_Version
hide_timestamp: true
image:
- file: mdi:power
id: power
resize: 28x28
- file: mdi:fire
id: fire
resize: 28x28
- file: mdi:chevron-up
id: chevron_up
resize: 70x70
- file: mdi:chevron-down
id: chevron_down
resize: 70x70
- file: mdi:home-assistant
id: home_assistant
resize: 90x90
- file: mdi:cogs
id: cogs
resize: 50x50
- file: mdi:home
id: home
resize: 50x50
font:
- file: "fonts/Roboto-Regular.ttf"
id: big_font
size: 40
- file: "fonts/Roboto-Regular.ttf"
id: little_font
size: 20
- file: "fonts/Roboto-Bold.ttf"
id: bold_font
size: 25
color:
- id: grey
hex: D1D0CE
- id: white
hex: FFFFFF
- id: dark_grey
hex: A0A0A0
- id: HA_blue
hex: 03a9f4
display:
- platform: ili9xxx
model: ili9341
id: my_display
pages:
- id: homepage
lambda: |-
it.fill(id(white));
it.strftime(5, 5, id(bold_font), Color::BLACK, "%H:%M", id(my_time).now());
it.printf(5, 35, id(bold_font), Color::BLACK, "Temperature:");
it.printf(110, 63, id(little_font), Color::BLACK, "%.1f°C", id(temp1).state);
it.printf(225, 70, id(big_font), Color::BLACK, "%.1f", id(my_climate).target_temperature);
it.image(235, 5, id(chevron_up), Color::BLACK);
it.image(235,110, id(chevron_down), Color::BLACK);
it.image(5, 80, id(home_assistant), id(HA_blue));
it.image(20, 180, id(cogs), id(dark_grey));
if (id(my_climate).mode != CLIMATE_MODE_HEAT) {
it.filled_rectangle(90, 180, 110, 55, id(HA_blue));
it.image(135, 182, id(power), id(white));
it.print(133, 207, id(little_font), id(white), "Off");
it.filled_rectangle(205, 180, 110, 55, id(grey));
it.print(240, 207, id(little_font), id(dark_grey), "Heat");
it.image(250, 182, id(fire), id(dark_grey));
} else {
it.filled_rectangle(90, 180, 110, 55, id(grey));
it.image(135, 182, id(power), id(dark_grey));
it.print(133, 207, id(little_font), id(dark_grey), "Off");
it.filled_rectangle(205, 180, 110, 55, id(HA_blue));
it.print(240, 207, id(little_font), id(white), "Heat");
it.image(250, 182, id(fire), id(white));
}
- id: settings_page
lambda: |-
it.fill(id(white));
it.image(10, 10, id(home), id(dark_grey));
it.print(90, 10, id(bold_font), Color::BLACK, "Settings");
it.printf(5, 60, id(little_font), Color::BLACK, "ESPHome Version: %s",id(ESPHome_Version).state.c_str());
it.printf(5, 85, id(little_font), Color::BLACK, "IP Address: %s", id(IP_Address).state.c_str());
it.printf(5, 110, id(little_font), Color::BLACK, "Uptime: %.f minutes",id(ESP_uptime).state);
it.printf(5, 135, id(little_font), Color::BLACK, "Wifi Signal: %.0f dBm",id(ESP_wifi_signal).state);
it.printf(5, 160, id(little_font), Color::BLACK, "HA Status: %s", id(HA_status).state ? "Connected" : "Disconnected");
dc_pin: 21
cs_pin: 33
reset_pin: 16
dimensions:
height: 240
width: 320
transform:
swap_xy: true
mirror_y: true
mirror_x: true
touchscreen:
platform: xpt2046
calibration_x_min: 323
calibration_x_max: 3769
calibration_y_min: 438
calibration_y_max: 3769
transform:
swap_xy: true
id: my_touchscreen
cs_pin: 32
update_interval: 50ms
threshold: 400
on_touch:
- binary_sensor.template.publish:
id: touching
state: ON
on_release:
- binary_sensor.template.publish:
id: touching
state: OFF
binary_sensor:
- platform: template
id: touching
filters:
delayed_off: 20s
on_press:
- light.control:
id: back_light
brightness: 100%
on_release:
- light.control:
id: back_light
brightness: 35%
- platform: status
name: "Thermostat One State"
id: HA_status
- platform: touchscreen
id: temp_down
x_min: 235
x_max: 295
y_min: 110
y_max: 160
page_id: homepage
on_press:
then:
- climate.control:
id: my_climate
target_temperature: !lambda return id(my_climate).target_temperature - 0.5;
- platform: touchscreen
id: temp_up
x_min: 235
x_max: 295
y_min: 20
y_max: 70
page_id: homepage
on_press:
then:
- climate.control:
id: my_climate
target_temperature: !lambda return id(my_climate).target_temperature + 0.5;
- platform: touchscreen
id: heating_on
x_min: 210
x_max: 315
y_min: 180
y_max: 235
page_id: homepage
on_press:
then:
- climate.control:
id: my_climate
mode: HEAT
- platform: touchscreen
id: heating_off
x_min: 90
x_max: 195
y_min: 180
y_max: 235
page_id: homepage
on_press:
then:
- climate.control:
id: my_climate
mode: "OFF"
- platform: touchscreen
id: settings_menu
x_min: 20
x_max: 70
y_min: 180
y_max: 235
page_id: homepage
on_press:
- display.page.show: settings_page
- component.update: my_display
- platform: touchscreen
id: home_page_menu
x_min: 10
x_max: 60
y_min: 10
y_max: 60
page_id: settings_page
on_press:
- display.page.show: homepage
- component.update: my_display
dallas:
pin: 14
update_interval: 10s
sensor:
- platform: dallas
address: 0x5d03139779b91928
id: temp1
name: "Temperature sensor"
- platform: uptime
id: ESP_uptime
filters:
- lambda: return x / 60.0;
unit_of_measurement: minutes
- platform: wifi_signal
id: ESP_wifi_signal
update_interval: 60s
switch:
- platform: gpio
pin: 22
id: led_one
# Define a PWM output on the ESP32
output:
- platform: ledc
pin: 17
id: gpio_17_backlight_pwm
# Define a monochromatic, dimmable light for the backlight
light:
- platform: monochromatic
output: gpio_17_backlight_pwm
name: "Display Backlight"
id: back_light
restore_mode: RESTORE_DEFAULT_ON
climate:
- platform: thermostat
name: "My Thermostat"
id: my_climate
sensor: temp1
heat_deadband: 0.5
heat_overrun: 0.5
min_heating_off_time: 6s
min_heating_run_time: 6s
min_idle_time: 6s
heat_action:
- switch.turn_on: led_one
idle_action:
- switch.turn_off: led_one
and here are some photos of the soldered circuit, complete with working relay and the new UI changes.
admittedly not my finest soldering as my best iron broke so I had to use a very old iron with no heating controls
all powered from 240V mains.
later today I will be working on a circuit diagram that I will post of the complete circuit
Here is the Circuit schematic for the above prototype, its not the best layout, Ive not had much experience creating circuit diagrams. Hopefully its legible enough to gain enough of an insight, plus the pinout from the yaml above should also help.
Glad to see you using female headers and then plugging devices into it. It kind of makes me cringe when i see people solder everything to a prototype board. I learned that lesson quickly and its really convenient being able to pull things off if they need replaced.
Be very careful with separating the heat from the ESP and screen from the temperature sensor. I’m using a smaller ESP8266 and little OLED screen and still had to have a couple of goes to avoid self-heating. My case now looks like this:
The Dallas sensor is behind the “front bars” insulated from the D1 Mini which is in the wall cavity with some expanded polystyrene foam and a plastic “lid”. The vents on the bottom and top channel airflow for the D1 Mini and the screen away from the sensor. I also turned the wifi transmission power right down to avoid heating.
I couldn’t agree more, the other components are cheap and easier to solder/de-solder when needing to be replaced but pin headers on displays or micro-controllers/SOC are a nightmare to de-solder. I will probably have a female pin header for the display on the final pcb also, not only for easy replacement but also, so that I can mount the pcb and the rear case to the electrical back box where my current thermostat is and then mount the display and front cover after.
nice, I like the case very sleek. Yeah I learnt this lesson the hard way, when creating an EspHome temp sensors years ago, I couldn’t work out why my sensor readings were so far off compared to a proprietary sensor I had for measurement.
Don’t apologize. Your schematic is millions of times better than Fritzing crap.
@GreyLinux thank you for taking the time to document your journey - you have helped me tremendously. I’ve been inspired to share my progress as well in case anyone would find it helpful for themselves.
I used the following esp32 device and display:
http://amazon.com/gp/product/B0B1M9S9V6
https://www.amazon.com/dp/B08D5ZD528
substitutions:
node_id: "master_bedroom_thermostat"
globals:
- id: temp_in_fahrenheit
restore_value: 'no'
type: float
- id: climate_mode
restore_value: 'no'
type: std::string
- id: active_climate_action
restore_value: 'no'
type: std::string
time:
- platform: homeassistant
id: my_time
on_time:
- seconds: 0
minutes: /1
then:
- component.update: tft_display
# i2c:
# - id: bus_a
# sda: 47 # minid1 - 21 # Blue
# scl: 48 # minid1 - 22 # Green
# scan: true
### >>>>>>>>>>>>> Text Sensors <<<<<<<<<<<<<<< ###
text_sensor:
# Last Rain - date time
- platform: homeassistant
name: outdoor_last_rain_sensor
id: outdoor_last_rain_sensor
entity_id: sensor.my_weather_station_last_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
### >>>>>>>>>>>>> Sensors <<<<<<<<<<<<<<< ###
sensor:
- platform: uptime
id: ${node_id}_uptime
name: Uptime
- platform: homeassistant
name: temp_sensor_celsius
id: temp_sensor_celsius
entity_id: sensor.master_bedroom_mmwave_sensor_temp
internal: true
filters:
- lambda:
id(temp_in_fahrenheit) = x;
return (x - 32) * 5/9;
on_value:
then:
- component.update: tft_display
- platform: homeassistant
name: humidity_sensor
id: humidity_sensor
entity_id: sensor.master_bedroom_mmwave_sensor_humidity
internal: true
on_value:
then:
- component.update: tft_display
### >>>>>>>>>>>>> Weather Station Sensors <<<<<<<<<<<<<<< ###
# Temp
- platform: homeassistant
name: outdoor_temp_sensor
id: outdoor_temp_sensor
entity_id: sensor.my_weather_station_temp
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Humidity
- platform: homeassistant
name: outdoor_humidity_sensor
id: outdoor_humidity_sensor
entity_id: sensor.my_weather_station_humidity
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Feels Like Temp
- platform: homeassistant
name: outdoor_feels_like_temp_sensor
id: outdoor_feels_like_temp_sensor
entity_id: sensor.my_weather_station_feels_like
internal: true
on_value:
then:
- component.update: tft_display
# Absolute Pressure
- platform: homeassistant
name: outdoor_absolute_pressure_sensor
id: outdoor_absolute_pressure_sensor
entity_id: sensor.my_weather_station_abs_pressure
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Relative Pressure
- platform: homeassistant
name: outdoor_relative_pressure_sensor
id: outdoor_relative_pressure_sensor
entity_id: sensor.my_weather_station_rel_pressure
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Dew Point
- platform: homeassistant
name: outdoor_dew_point_sensor
id: outdoor_dew_point_sensor
entity_id: sensor.my_weather_station_dew_point
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Solar UV Index
- platform: homeassistant
name: outdoor_solar_uv_index_sensor
id: outdoor_solar_uv_index_sensor
entity_id: sensor.my_weather_station_uv_index
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Solar Rad W/m2
- platform: homeassistant
name: outdoor_solar_rad_watts_sensor
id: outdoor_solar_rad_watts_sensor
entity_id: sensor.my_weather_station_solar_rad
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Solar Rad Lux
- platform: homeassistant
name: outdoor_solar_rad_lux_sensor
id: outdoor_solar_rad_lux_sensor
entity_id: sensor.my_weather_station_solar_rad_lx
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Wind Speed
- platform: homeassistant
name: outdoor_wind_speed_sensor
id: outdoor_wind_speed_sensor
entity_id: sensor.my_weather_station_wind_speed
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Wind Direction
- platform: homeassistant
name: outdoor_wind_direction_sensor
id: outdoor_wind_direction_sensor
entity_id: sensor.my_weather_station_wind_dir
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Wind Gust
- platform: homeassistant
name: outdoor_wind_gust_sensor
id: outdoor_wind_gust_sensor
entity_id: sensor.my_weather_station_wind_gust
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Event Rain
- platform: homeassistant
name: outdoor_event_rain_sensor
id: outdoor_event_rain_sensor
entity_id: sensor.my_weather_station_event_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Hourly Rate Rain
- platform: homeassistant
name: outdoor_hourly_rain_rate_sensor
id: outdoor_hourly_rain_rate_sensor
entity_id: sensor.my_weather_station_hourly_rain_rate
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Daily Rain
- platform: homeassistant
name: outdoor_daily_rain_sensor
id: outdoor_daily_rain_sensor
entity_id: sensor.my_weather_station_daily_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Weekly Rain
- platform: homeassistant
name: outdoor_weekly_rain_sensor
id: outdoor_weekly_rain_sensor
entity_id: sensor.my_weather_station_weekly_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Monthly Rain
- platform: homeassistant
name: outdoor_monthly_rain_sensor
id: outdoor_monthly_rain_sensor
entity_id: sensor.my_weather_station_monthly_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Yearly Rain
- platform: homeassistant
name: outdoor_yearly_rain_sensor
id: outdoor_yearly_rain_sensor
entity_id: sensor.my_weather_station_yearly_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
# Lifetime Rain
- platform: homeassistant
name: outdoor_lifetime_rain_sensor
id: outdoor_lifetime_rain_sensor
entity_id: sensor.my_weather_station_lifetime_rain
internal: true
on_value:
if:
condition:
- lambda: 'return id(tft_display).get_active_page() == id(weather_station_page);'
then:
- component.update: tft_display
### >>>>>>>>>>>>> BME280 SENSOR <<<<<<<<<<<<<<< ###
# - platform: bme280_i2c
# temperature:
# id: ${node_id}_temp_sensor
# name: BME280 Temp
# accuracy_decimals: 1
# oversampling: 16x
# pressure:
# id: ${node_id}_pressure
# name: BME280 Pressure
# oversampling: 2x
# humidity:
# id: ${node_id}_humidity
# name: BME280 Humidity
# accuracy_decimals: 1
# oversampling: 8x
# address: 0x76
# update_interval: 30s
### >>>>>>>>>>>>> Switches <<<<<<<<<<<<<<< ###
switch:
- platform: restart # Restart
name: Restart
id: ${node_id}_restart
icon: "mdi:restart"
- platform: shutdown # Shutdown
name: Shutdown
id: ${node_id}_shutdown
- platform: safe_mode # Safe Mode
name: Restart (Safe Mode)"
id: ${node_id}_safe_mode
- platform: template # HVAC Power - Source: https://community.home-assistant.io/t/switch-linked-to-homeassistant-switch/593354/3?u=bsell93
id: hvac_power
optimistic: true
turn_on_action:
- homeassistant.service:
service: switch.turn_on
data:
entity_id: switch.master_bedroom_hvac_power
- delay: 1s
- homeassistant.service:
service: climate.set_hvac_mode
data:
entity_id: climate.master_bedroom_hvac_climate
data_template:
hvac_mode: '{{ mode }}'
variables:
mode: |-
return id(climate_mode);
- delay: 1s
- homeassistant.service:
service: climate.set_temperature
data:
entity_id: climate.master_bedroom_hvac_climate
data_template:
temperature: '{{ target_temp }}'
variables:
target_temp: |-
if (id(${node_id}_climate).mode == CLIMATE_MODE_COOL)
{
return round(id(${node_id}_climate).target_temperature_high * (9.0/5.0) + 32.0) - 1;
}
return round(id(${node_id}_climate).target_temperature_low * (9.0/5.0) + 32.0) + 1;
turn_off_action:
- homeassistant.service:
service: switch.turn_off
data:
entity_id: switch.master_bedroom_hvac_power
### >>>>>>>>>>>>> Climate <<<<<<<<<<<<<<< ###
climate:
- platform: thermostat
name: "Thermostat"
id: ${node_id}_climate
sensor: temp_sensor_celsius
humidity_sensor: humidity_sensor
min_cooling_off_time: 1s # 300s
min_cooling_run_time: 1s # 300s
min_heating_off_time: 1s # 300s
min_heating_run_time: 1s # 300s
min_idle_time: 30s
visual:
temperature_step:
target_temperature: 1
current_temperature: 1
cool_action:
- lambda: |-
id(climate_mode) = "cool";
id(active_climate_action) = "cooling";
- switch.turn_on: hvac_power
heat_action:
- lambda: |-
id(climate_mode) = "heat";
id(active_climate_action) = "heating";
- switch.turn_on: hvac_power
idle_action:
- lambda: |-
id(active_climate_action) = "idle";
- switch.turn_off: hvac_power
on_state:
then:
if:
condition:
- lambda: |-
return id(active_climate_action) != "idle";
then:
- homeassistant.service:
service: climate.set_temperature
data:
entity_id: climate.master_bedroom_hvac_climate
data_template:
hvac_mode: '{{ hvac_mode }}'
temperature: '{{ target_temp }}'
variables:
hvac_mode: |-
auto mode = id(${node_id}_climate).mode;
if (mode == CLIMATE_MODE_COOL)
{
return "cool";
}
else if (mode == CLIMATE_MODE_HEAT)
{
return "heat";
}
return "off";
target_temp: |-
if (id(${node_id}_climate).mode == CLIMATE_MODE_COOL)
{
return round(id(${node_id}_climate).target_temperature_high * (9.0/5.0) + 32.0) - 1;
}
return round(id(${node_id}_climate).target_temperature_low * (9.0/5.0) + 32.0) + 1;
default_preset: Home
on_boot_restore_from: memory
preset:
- name: Home
mode: cool
default_target_temperature_low: 73 °F
default_target_temperature_high: 73 °F
- name: Sleep
mode: cool
default_target_temperature_low: 70 °F
default_target_temperature_high: 70 °F
- name: Away
mode: cool
default_target_temperature_low: 76 °F
default_target_temperature_high: 76 °F
### >>>>>>>>>>>>> Image <<<<<<<<<<<<<<< ###
image:
- file: mdi:close
id: close_icon
resize: 28x28
- file: mdi:power
id: power_icon
resize: 28x28
- file: mdi:fire
id: heat_icon
resize: 28x28
- file: mdi:snowflake
id: cool_icon
resize: 28x28
- file: mdi:minus
id: minus_icon
resize: 40x40
- file: mdi:plus
id: plus_icon
resize: 40x40
### >>>>>>>>>>>>> Font <<<<<<<<<<<<<<< ###
font:
# gfonts://family[@weight]
- file: "gfonts://Roboto"
id: roboto_48
size: 48
- file: "gfonts://Roboto"
id: roboto_24
size: 24
- file: "gfonts://Roboto"
id: roboto_16
size: 16
- file: "gfonts://Roboto"
id: roboto_10
size: 10
### >>>>>>>>>>>>> Color <<<<<<<<<<<<<<< ###
color:
- id: heat_red
hex: ec780f
- id: cool_blue
hex: 096dff
- id: off_green
hex: 08f26d
- id: grey
hex: D1D0CE
- id: dark_grey
hex: A0A0A0
### >>>>>>>>>>>>> SPI <<<<<<<<<<<<<<< ###
spi:
clk_pin: GPIO18 # minid1 - 18; esp32s3 - 11 # Brown
mosi_pin: GPIO23 # minid1 - 23; esp32s3 - 13 # Blue
miso_pin: GPIO19 # minid1 - 19; esp32s3 - 12 # Yellow
### >>>>>>>>>>>>> Display <<<<<<<<<<<<<<< ###
display:
- platform: ili9xxx
model: ILI9341
id: tft_display
dc_pin: GPIO21 # minid1 - 21; esp32s3 - 14 # Purple
cs_pin: GPIO33 # minid1 - 33; esp32s3 - 10 # White
reset_pin: GPIO26 # minid1 - 26; esp32s3 - 9 # Orange
dimensions:
height: 240
width: 320
update_interval: never
# auto_clear_enabled: false
data_rate: 40MHz
transform:
swap_xy: true
mirror_y: true
mirror_x: true
pages:
### >>>>>>>>>>>>> Home Page <<<<<<<<<<<<<<< ###
- id: home_page
lambda: |-
// alignment lines
// it.line(it.get_width()/2, 0, it.get_width()/2, it.get_height());
// it.line(0, it.get_height()/2, it.get_width(), it.get_height()/2);
// it.line(it.get_width()* 1/4, 0, it.get_width()* 1/4, it.get_height());
// it.line(it.get_width()* 3/4, 0, it.get_width()* 3/4, it.get_height());
// it.line(it.get_width()* 1/3, 0, it.get_width()* 1/3, it.get_height());
// it.line(it.get_width()* 2/3, 0, it.get_width()* 2/3, it.get_height());
// Show time
it.strftime(5, 5, id(roboto_16), "%I:%M %p", id(my_time).now());
// Show outdoor feels like temp and humidity
it.printf(it.get_width() - 5, 5, id(roboto_16), TextAlign::TOP_RIGHT, "%.0f°", id(outdoor_feels_like_temp_sensor).state);
// Set Current Humidity
it.printf(it.get_width()/2, (it.get_height()/2) - 50, id(roboto_16), TextAlign::CENTER, "%.0f%%", id(humidity_sensor).state);
// Set Current Temp
it.printf(it.get_width()/2, it.get_height()/2, id(roboto_48), TextAlign::CENTER, "%.0f°", id(temp_in_fahrenheit));
// Set Target Temp
auto current_mode = id(${node_id}_climate).mode;
auto target_temp = id(${node_id}_climate).target_temperature * (9.0/5.0) + 32.0; // Convert to Fahrenheit
auto current_mode_color = id(off_green);
auto active_mode_icon = id(power_icon); // mode icon
auto is_on = false;
if (current_mode == CLIMATE_MODE_HEAT)
{
target_temp = id(${node_id}_climate).target_temperature_low * (9.0/5.0) + 32.0;
current_mode_color = id(heat_red);
active_mode_icon = id(heat_icon);
is_on = true;
}
else if (current_mode == CLIMATE_MODE_COOL)
{
target_temp = id(${node_id}_climate).target_temperature_high * (9.0/5.0) + 32.0;
current_mode_color = id(cool_blue);
active_mode_icon = id(cool_icon);
is_on = true;
}
if (is_on)
{
// Plus/minus icons
it.image((it.get_width()* 1/3) - 20, it.get_height()/2 - 20, id(minus_icon));
it.image((it.get_width()* 2/3) - 20, it.get_height()/2 - 20, id(plus_icon));
// Target Temp
it.printf(it.get_width()/2, (it.get_height()/2) + 50, id(roboto_24), current_mode_color, TextAlign::CENTER, "%.0f°", target_temp);
// Draw rectangle around target temp to indicate button
it.rectangle((it.get_width()/2) - 25, (it.get_height()/2) + 32.5, 50, 37.5, current_mode_color);
}
// Mode icon
it.image(5, it.get_height() - 33, active_mode_icon);
### >>>>>>>>>>>>> Mode Page <<<<<<<<<<<<<<< ###
- id: mode_page
lambda: |-
// Show time
it.strftime(5, 5, id(roboto_16), "%I:%M %p", id(my_time).now());
// Show outdoor feels like temp and humidity
it.printf(it.get_width() - 5, 5, id(roboto_16), TextAlign::TOP_RIGHT, "%.0f°", id(outdoor_feels_like_temp_sensor).state);
auto current_mode = id(${node_id}_climate).mode;
auto heat_mode_color = id(dark_grey);
auto cool_mode_color = id(dark_grey);
auto power_mode_color = id(dark_grey);
if (current_mode == CLIMATE_MODE_HEAT)
{
heat_mode_color = id(heat_red);
}
else if (current_mode == CLIMATE_MODE_COOL)
{
cool_mode_color = id(cool_blue);
}
else
{
power_mode_color = id(off_green);
}
// Render Cool mode button
it.rectangle((it.get_width() * 1/4) - 30, (it.get_height()/2) - 25, 60, 55, cool_mode_color);
it.image((it.get_width() * 1/4) - 14, (it.get_height()/2) - 22, id(cool_icon), cool_mode_color);
it.print((it.get_width() * 1/4) - 25, it.get_height()/2, id(roboto_24), cool_mode_color, "Cool");
// Render Power mode button
it.rectangle((it.get_width()/2) - 30, (it.get_height()/2) - 25, 60, 55, power_mode_color);
it.image((it.get_width()/2) - 14, (it.get_height()/2) - 22, id(power_icon), power_mode_color);
it.print((it.get_width()/2) - 17, it.get_height()/2, id(roboto_24), power_mode_color, "Off");
// Render Heat mode button
it.rectangle((it.get_width() * 3/4) - 30, (it.get_height()/2) - 25, 60, 55, heat_mode_color);
it.image((it.get_width() * 3/4) - 14, (it.get_height()/2) - 22, id(heat_icon), heat_mode_color);
it.print((it.get_width() * 3/4) - 25, it.get_height()/2, id(roboto_24), heat_mode_color, "Heat");
### >>>>>>>>>>>>> Weather Station Page <<<<<<<<<<<<<<< ###
- id: weather_station_page
lambda: |-
// Show time
it.strftime(5, 5, id(roboto_16), "%I:%M %p", id(my_time).now());
// Show X - close button top right
it.image(it.get_width() - 5, 5, id(close_icon), ImageAlign::TOP_RIGHT);
// Temp
it.printf(5, it.get_height() * 1/10, id(roboto_10), "Temperature: %.0f°", id(outdoor_temp_sensor).state);
// Humidity
it.printf(5, it.get_height() * 2/10, id(roboto_10), "Humidity: %.0f%%", id(outdoor_humidity_sensor).state);
// Feels Like Temp
it.printf(5, it.get_height() * 3/10, id(roboto_10), "Feels Like: %.0f°", id(outdoor_feels_like_temp_sensor).state);
// Absolute Pressure
it.printf(5, it.get_height() * 4/10, id(roboto_10), "Abs Pressure: %.0finHg", id(outdoor_absolute_pressure_sensor).state);
// Relative Pressure
it.printf(5, it.get_height() * 5/10, id(roboto_10), "Rel Pressure: %.0finHg", id(outdoor_relative_pressure_sensor).state);
// Dew Point
it.printf(5, it.get_height() * 6/10, id(roboto_10), "Dew Point: %.0f°", id(outdoor_dew_point_sensor).state);
// Solar UV Index
it.printf(5, it.get_height() * 7/10, id(roboto_10), "Solar UV Index: %.0f", id(outdoor_solar_uv_index_sensor).state);
// Solar Rad W/m2 & Lux
it.printf(5, it.get_height() * 8/10, id(roboto_10), "Solar Rad: %.0fW/m2 (%.0f lux)", id(outdoor_solar_rad_watts_sensor).state, id(outdoor_solar_rad_lux_sensor).state);
// Wind Speed & Direction
it.printf(5, it.get_height() * 9/10, id(roboto_10), "Wind: %.0fmph (%.0f°)", id(outdoor_wind_speed_sensor).state, id(outdoor_wind_direction_sensor).state);
// Wind Gust
it.printf(it.get_width()/2, it.get_height() * 9/10, id(roboto_10), "Wind Gust: %.0fmph", id(outdoor_wind_gust_sensor).state);
// Event Rain
it.printf(it.get_width()/2, it.get_height() * 1/10, id(roboto_10), "Event Rain: %.0fin", id(outdoor_event_rain_sensor).state);
// Hourly Rain Rate
it.printf(it.get_width()/2, it.get_height() * 2/10, id(roboto_10), "Hourly Rain Rate: %.0fin", id(outdoor_hourly_rain_rate_sensor).state);
// Last Rain - date time
auto last_rain = id(outdoor_last_rain_sensor).state;
it.printf(it.get_width()/2, it.get_height() * 3/10, id(roboto_10), "Last Rain: %s %s", last_rain.substr(0,10).c_str(), last_rain.substr(11, 5).c_str());
// Daily Rain
it.printf(it.get_width()/2, it.get_height() * 4/10, id(roboto_10), "Daily Rain: %.0fin", id(outdoor_daily_rain_sensor).state);
// Weekly Rain
it.printf(it.get_width()/2, it.get_height() * 5/10, id(roboto_10), "Weekly Rain: %.0fin", id(outdoor_weekly_rain_sensor).state);
// Monthly Rain
it.printf(it.get_width()/2, it.get_height() * 6/10, id(roboto_10), "Monthly Rain: %.0fin", id(outdoor_monthly_rain_sensor).state);
// Yearly Rain
it.printf(it.get_width()/2, it.get_height() * 7/10, id(roboto_10), "Yearly Rain: %.0fin", id(outdoor_yearly_rain_sensor).state);
// Lifetime Rain
it.printf(it.get_width()/2, it.get_height() * 8/10, id(roboto_10), "Lifetime Rain: %.0fin", id(outdoor_lifetime_rain_sensor).state);
### >>>>>>>>>>>>> Touch Screen <<<<<<<<<<<<<<< ###
touchscreen:
platform: xpt2046
id: my_touchscreen
cs_pin: GPIO32 # White
update_interval: 50ms
threshold: 400
transform:
swap_xy: true
calibration:
x_min: 203
x_max: 3839
y_min: 340
y_max: 3849
on_touch:
- binary_sensor.template.publish:
id: touching
state: ON
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
on_release:
- binary_sensor.template.publish:
id: touching
state: OFF
### >>>>>>>>>>>>> Binary Sensors <<<<<<<<<<<<<<< ###
binary_sensor:
### >>>>>>>>>>>>> Home Page Touch Areas <<<<<<<<<<<<<<< ###
- platform: touchscreen
id: temp_down
x_min: 70
x_max: 130
y_min: 100
y_max: 150
page_id: home_page
on_press:
then:
- climate.control:
id: ${node_id}_climate
target_temperature_low: !lambda
auto current_mode = id(${node_id}_climate).mode;
if (current_mode == CLIMATE_MODE_HEAT)
{
auto target_temp_in_f = id(${node_id}_climate).target_temperature_low * (9.0/5.0) + 32.0;
return ((target_temp_in_f - 1) - 32) * 5/9;
}
auto lowest_temp = 50;
return ((lowest_temp) - 32) * 5/9; // Convert back to celsius
target_temperature_high: !lambda
auto current_mode = id(${node_id}_climate).mode;
if (current_mode == CLIMATE_MODE_COOL)
{
auto target_temp_in_f = id(${node_id}_climate).target_temperature_high * (9.0/5.0) + 32.0;
return ((target_temp_in_f - 1) - 32) * 5/9;
}
auto highest_temp = 86;
return ((highest_temp) - 32) * 5/9; // Convert back to celsius
- component.update: tft_display
- platform: touchscreen
id: temp_up
x_min: 205
x_max: 260
y_min: 100
y_max: 150
page_id: home_page
on_press:
then:
- climate.control:
id: ${node_id}_climate
target_temperature_low: !lambda
auto current_mode = id(${node_id}_climate).mode;
if (current_mode == CLIMATE_MODE_HEAT)
{
auto target_temp_in_f = id(${node_id}_climate).target_temperature_low * (9.0/5.0) + 32.0;
return ((target_temp_in_f + 1) - 32) * 5/9;
}
auto lowest_temp = 50;
return ((lowest_temp) - 32) * 5/9; // Convert back to celsius
target_temperature_high: !lambda
auto current_mode = id(${node_id}_climate).mode;
if (current_mode == CLIMATE_MODE_COOL)
{
auto target_temp_in_f = id(${node_id}_climate).target_temperature_high * (9.0/5.0) + 32.0;
return ((target_temp_in_f + 1) - 32) * 5/9;
}
auto highest_temp = 86;
return ((highest_temp) - 32) * 5/9; // Convert back to celsius
- component.update: tft_display
- platform: touchscreen
id: mode_select
x_min: 0
x_max: 55
y_min: 205
y_max: 240
page_id: home_page
on_press:
then:
- display.page.show: mode_page
- component.update: tft_display
- platform: touchscreen
id: open_weather_button
x_min: 270
x_max: 320
y_min: 0
y_max: 50
page_id: home_page
on_release:
then:
- display.page.show: weather_station_page
- component.update: tft_display
### >>>>>>>>>>>>> Mode Page Touch Areas <<<<<<<<<<<<<<< ###
- platform: touchscreen
id: set_cool_mode
x_min: 50
x_max: 115
y_min: 90
y_max: 150
page_id: mode_page
on_press:
then:
- climate.control:
id: ${node_id}_climate
mode: COOL
- display.page.show: home_page
- component.update: tft_display
- platform: touchscreen
id: set_off_mode
x_min: 135
x_max: 200
y_min: 90
y_max: 150
page_id: mode_page
on_press:
then:
- climate.control:
id: ${node_id}_climate
mode: "OFF"
- display.page.show: home_page
- component.update: tft_display
- platform: touchscreen
id: set_heat_mode
x_min: 220
x_max: 285
y_min: 90
y_max: 150
page_id: mode_page
on_press:
then:
- climate.control:
id: ${node_id}_climate
mode: "HEAT"
- display.page.show: home_page
- component.update: tft_display
### >>>>>>>>>>>>> Weather Page Touch Areas <<<<<<<<<<<<<<< ###
- platform: touchscreen
id: close_button
x_min: 270
x_max: 320
y_min: 0
y_max: 50
page_id: weather_station_page
on_release:
then:
- display.page.show: home_page
- component.update: tft_display
### >>>>>>>>>>>>> Backlight Binary Sensor <<<<<<<<<<<<<<< ###
- platform: template
id: touching
filters:
delayed_off: 20s
on_press:
- light.control:
id: back_light
brightness: 100%
on_release:
- light.control:
id: back_light
brightness: 35%
### >>>>>>>>>>>>> Backlight <<<<<<<<<<<<<<< ###
# Define a PWM output on the ESP32
output:
- platform: ledc
pin: 25
id: backlight_output
# Define a monochromatic, dimmable light for the backlight
light:
- platform: monochromatic
output: backlight_output
name: "Display Backlight"
id: back_light
restore_mode: RESTORE_DEFAULT_ON