Work in progress! esphome thermostat

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.

1 Like

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





5 Likes

Update:

I have made this config reusable and also added in the HLK LD2450 sensor and BH1750 Illuminance sensor. I also have in the works a separate temp/humidity/pressure sensor using BME280 and esp-01s - I found putting the BME280 in a case with the display and all the other sensors was making the temp reading wildly inaccurate as well as having the flexibility to place a temp sensor in a desired location I think is a plus.

Here is some updated config code

The following us a usage of the template:

# Enable Home Assistant API
api:
  encryption:
    key: "<key>"

ota:
  password: "<pwd>"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "<ssid>"
    password: "<ap-pwd>"
    
substitutions:
  node_id: some_thermostat
  thermostat_name: some-thermostat
  thermostat_friendly_name: Some Thermostat
  temp_sensor_entity_id: sensor.some_temp_sensor
  humidity_sensor_entity_id: sensor.some_humdity_sensor
  hvac_power_entity_id: switch.some_hvac_power
  hvac_climate_entity_id: climate.some_hvac_climate

<<: !include .thermostat.common.yaml

The following is the template that allows scaling multiple devices without copy paste (was going to copy paste it all here, but there were too many characters):

I also designed a case here:

1 Like