substitutions: color_bg: '0x282A36' color_bg_alt: '0x34353e' color_white: '0xE2E4E5' color_temp_current: '0x57c7ff' color_dark_blue: '0x57c7ff' color_light_blue: '0x9aedfe' color_green: '0x5AF78E' color_red: '0xff5c57' color_darker_white: '0xA5A5A9' esphome: name: thermostat esp32: board: esp32s3box variant: esp32s3 flash_size: 8MB framework: type: esp-idf version: latest sdkconfig_options: CONFIG_IDF_TARGET: "esp32s3" CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240: y CONFIG_ESPTOOLPY_FLASHMODE_QIO: y CONFIG_ESPTOOLPY_FLASHFREQ_80M: y CONFIG_ESPTOOLPY_FLASHSIZE_8MB: y CONFIG_IDF_EXPERIMENTAL_FEATURES: y CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y CONFIG_SPIRAM: y CONFIG_SPIRAM_MODE_OCT: y CONFIG_SPIRAM_RODATA: y CONFIG_SPIRAM_SPEED_80M: y CONFIG_FREERTOS_HZ: "1000" CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y COMPILER_OPTIMIZATION_PERF: y psram: mode: octal speed: 80MHz # Enable logging logger: level: DEBUG # baud_rate: 0 ch422g: # address: 0x24 # Enable Home Assistant API api: reboot_timeout: 10s password: "" ota: - platform: esphome password: "" wifi: ssid: "Cochrun" password: "GreasyChicken784" power_save_mode: none reboot_timeout: 10s manual_ip: static_ip: 192.168.1.123 gateway: 192.168.1.1 subnet: 255.255.255.0 # ssid: "TFC" # password: "Disciple77" ap: ssid: "Thermostat Fallback Hotspot" password: "RUfAhbEfZmOU" captive_portal: i2c: - id: bus_a sda: GPIO08 scl: GPIO09 scan: True frequency: 400kHz time: - platform: homeassistant id: hass_time on_time: - seconds: /5 # Update every second then: - lvgl.label.update: id: time_label text: !lambda |- auto time = id(hass_time).now(); return time.strftime("%a %b %e, %I:%M %p"); # - script.execute: update_date_time_label sensor: - platform: bme280_i2c temperature: name: "Temperature" id: temperature filters: - offset: 1.0 on_value: then: - lvgl.label.update: id: temp_label text: !lambda |- auto temp = x * (9.0/5.0) + 32.0; return str_truncate(to_string(temp), 4); - lvgl.indicator.update: id: current_temp_needle value: !lambda |- return x * 10.0; humidity: name: "Humidity" id: humidity pressure: name: "Pressure" id: pressure address: 0x76 switch: - platform: homeassistant id: heater entity_id: switch.hvac_heat - platform: homeassistant id: ac entity_id: switch.hvac_ac - platform: homeassistant id: fan entity_id: switch.hvac_fan climate: - platform: thermostat id: climape name: "Thermostat" sensor: temperature humidity_sensor: humidity min_cooling_off_time: 10s min_cooling_run_time: 10s min_heating_off_time: 10s min_heating_run_time: 10s min_fanning_off_time: 10s min_fanning_run_time: 10s min_idle_time: 30s cool_action: - switch.turn_off: heater - switch.turn_on: ac - switch.turn_on: fan heat_action: - switch.turn_off: ac - switch.turn_on: heater - switch.turn_on: fan idle_action: - switch.turn_off: ac - switch.turn_off: heater - switch.turn_off: fan fan_only_action: - switch.turn_off: ac - switch.turn_off: heater - switch.turn_on: fan - lambda: !lambda ESP_LOGD("fan", "DA FAN"); default_preset: Home preset: - name: Home default_target_temperature_low: 67 °F default_target_temperature_high: 78 °F - name: Summer default_target_temperature_low: 60 °F default_target_temperature_high: 77 °F - name: Winter default_target_temperature_low: 68 °F default_target_temperature_high: 85 °F - name: Away default_target_temperature_low: 60 °F default_target_temperature_high: 85 °F on_state: then: - lvgl.indicator.update: id: hvac_temp_ticks start_value: !lambda |- return x.target_temperature_high * 10.0; end_value: !lambda |- return x.target_temperature_low * 10.0; - lvgl.arc.update: id: hvac_temp_knob value: !lambda |- return x.target_temperature_high * 10.0; - if: condition: lambda: "return x.mode != CLIMATE_MODE_OFF;" then: - lvgl.label.update: id: action_label text: format: "%s" args: [ 'climate_action_to_string(x.action)' ] else: - lvgl.label.update: id: set_temp_label text: "CLIMATE_OFF" - if: condition: lambda: "return x.mode == CLIMATE_MODE_COOL;" then: - lvgl.widget.update: id: cool_button state: checked: true - lvgl.widget.update: id: heat_button state: checked: false - lvgl.widget.update: id: fan_button state: checked: false - lvgl.widget.update: id: off_button state: checked: false # - lvgl.arc.update: # id: current_temp_arc # value: !lambda |- # return x.target_temperature_high * (9.0/5.0) + 32.0; - lvgl.label.update: id: set_temp_label text: format: "%.1f°" args: [ 'x.target_temperature_high * (9.0/5.0) + 32.0' ] - if: condition: lambda: "return x.mode == CLIMATE_MODE_HEAT;" then: - lvgl.widget.update: id: cool_button state: checked: false - lvgl.widget.update: id: heat_button state: checked: true - lvgl.widget.update: id: fan_button state: checked: false - lvgl.widget.update: id: off_button state: checked: false # - lvgl.arc.update: # id: current_temp_arc # value: !lambda |- # return x.target_temperature_low * (9.0/5.0) + 32.0; - lvgl.label.update: id: set_temp_label text: format: "%.1f°" args: [ 'x.target_temperature_low * (9.0/5.0) + 32.0' ] - if: condition: lambda: "return x.mode == CLIMATE_MODE_FAN_ONLY;" then: - lvgl.widget.update: id: cool_button state: checked: false - lvgl.widget.update: id: heat_button state: checked: false - lvgl.widget.update: id: fan_button state: checked: true - lvgl.widget.update: id: off_button state: checked: false # - lvgl.arc.update: # id: current_temp_arc # value: !lambda |- # return x.target_temperature_high * (9.0/5.0) + 32.0; - lvgl.label.update: id: set_temp_label text: format: "%.1f°" args: [ 'x.target_temperature_high * (9.0/5.0) + 32.0' ] - if: condition: lambda: "return x.mode == CLIMATE_MODE_OFF;" then: - lvgl.widget.update: id: cool_button state: checked: false - lvgl.widget.update: id: heat_button state: checked: false - lvgl.widget.update: id: fan_button state: checked: false - lvgl.widget.update: id: off_button state: checked: true # Example minimal configuration entry display: - platform: rpi_dpi_rgb id: my_display auto_clear_enabled: false update_interval: never color_order: RGB pclk_frequency: 16MHz dimensions: width: 800 height: 480 reset_pin: ch422g: number: 3 enable_pin: ch422g: number: 2 de_pin: number: 5 hsync_pin: number: 46 ignore_strapping_warning: true vsync_pin: number: 3 ignore_strapping_warning: true pclk_pin: 7 hsync_back_porch: 30 hsync_front_porch: 210 hsync_pulse_width: 30 vsync_back_porch: 4 vsync_front_porch: 4 vsync_pulse_width: 4 data_pins: red: [1, 2, 42, 41, 40] blue: [14, 38, 18, 17, 10] green: [39, 0, 45, 48, 47, 21] font: - file: type: gfonts family: Quicksand weight: 500 id: h2 size: 40 - file: type: gfonts family: Quicksand weight: 400 id: h3 size: 20 - file: type: gfonts family: Quicksand weight: 400 id: body size: 18 - file: type: gfonts family: Quicksand weight: 700 id: h1 size: 120 - file: "fonts/vm.ttf" id: md_icons size: 30 glyphs: [ "󰖩", # nf:wifi ] - file: "fonts/vm.ttf" id: sm_icons size: 20 glyphs: [ "󰖩", # nf:wifi ] - file: "fonts/materialdesignicons-webfont.ttf" id: icons size: 30 glyphs: [ "\U000F050F", # mdi:thermometer "\U000F058E", # mdi:water-percent "\U000F0425", # mdi:power "\U000F0238", # mdi:fire "\U000F1A45", # mdi:heat-wave "\U000F0717", # mdi:snowflake "\U000F1A79", # mdi:sun-snowflake-variant "\U000F0210", # mdi:fan "\U000F171D", # mdi:fan-auto "\U000F1472", # mdi:fan-speed-1 "\U000F1473", # mdi:fan-speed-2 "\U000F1474", # mdi:fan-speed-3 "\U000F032A", # mdi:leaf "\U000F04B9", # mdi:sofa "\U000F14DE", # mdi:rocket-launch "\U000F0D80", # mdi:home-floor-1 "\U000F0D81", # mdi:home-floor-2 "\U000F004D", # mdi:arrow-left "\U000F09DF", # mdi:circle-small "\U000F0E03", # mdi:thermometer-chevron-up "\U000F0E02", # mdi:thermometer-chevron-down "\U000F0594", # mdi:weather-night (clear) "\U000F0590", # mdi:weather-cloudy (cloudy) "\U000F0898", # mdi:weather-hurricane (cyclone, tropical_cyclone) "\U000F0F30", # mdi:weather-hazy (dust, dusty, haze, hazy) "\U000F0591", # mdi:weather-fog (fog) "\U000F12CB", # mdi:snowflake-melt (frost) "\U000F0596", # mdi:weather-pouring (heavy_shower, heavy_showers, rain) "\U000F0F33", # mdi:weather-partly-rainy (light_rain) # mdi:weather-light-showers (light_shower, light_showers) "\U000F0599", # mdi:weather-sunny (mostly_sunny, sunny) "\U000F0595", # mdi:weather-partly-cloudy (partly_cloudy) "\U000F0597", # mdi:weather-rainy (shower, showers) "\U000F0598", # mdi:weather-snowy (snow) "\U000F0593", # mdi:weather-lightning "\U000F067E", # mdi:weather-lightning-rainy (storm, storms) "\U000F059D", # mdi:weather-windy (wind, windy) "\U000F0592", # mdi:weather-hail ] touchscreen: - platform: gt911 id: my_touchscreen interrupt_pin: 4 reset_pin: ch422g: number: 1 on_touch: - lambda: |- ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d", touch.x, touch.y, touch.x_raw, touch.y_raw ); color: - id: bg hex: 282A36 lvgl: # buffer_size: 25% default_font: h2 theme: slider: bg_color: 0x57c7ff knob: bg_color: 0x57c7ff pages: - id: main_page bg_color: 0x282A36 widgets: - obj: align: center bg_color: 0x282A36 border_width: 0 height: 480 width: 800 scrollable: false widgets: - obj: bg_color: ${color_bg} width: 100% border_width: 0 widgets: - label: id: time_label text: "" text_color: ${color_darker_white} text_font: h3 align: top_left - label: id: wifi_label text: "󰖩" text_color: ${color_darker_white} align: top_right text_font: sm_icons - obj: bg_opa: 0 height: 480 width: 100% border_width: 0 widgets: - obj: align: BOTTOM_MID width: 100% bg_color: 0x282A36 border_width: 0 layout: type: flex pad_row: 4 pad_column: 4 flex_align_main: CENTER flex_align_cross: END flex_align_track: END widgets: - button: id: heat_button height: 60 width: 60 bg_color: 0x34353e border_width: 2 border_color: 0xE2E4E5 radius: 20 checkable: true checked: bg_color: 0xff5c57 on_click: then: - lvgl.widget.update: id: off_button state: checked: false - lvgl.widget.update: id: fan_button state: checked: false - lvgl.widget.update: id: cool_button state: checked: false - climate.control: id: climape mode: HEAT widgets: - label: text: "\U000F0238" align: center text_font: icons text_color: 0xE2E4E5 - button: id: cool_button height: 60 width: 60 bg_color: 0x34353e border_width: 2 border_color: 0xE2E4E5 radius: 20 checkable: true checked: bg_color: 0x9aedfe on_click: then: - lvgl.widget.update: id: off_button state: checked: false - lvgl.widget.update: id: fan_button state: checked: false - lvgl.widget.update: id: heat_button state: checked: false - climate.control: id: climape mode: COOL widgets: - label: text: "\U000F0717" align: center text_font: icons text_color: 0xE2E4E5 - button: id: fan_button height: 60 width: 60 bg_color: 0x34353e border_width: 2 border_color: 0xE2E4E5 radius: 20 checkable: true checked: bg_color: 0x5AF78E on_click: then: - lvgl.widget.update: id: off_button state: checked: false - lvgl.widget.update: id: cool_button state: checked: false - lvgl.widget.update: id: heat_button state: checked: false - climate.control: id: climape mode: FAN_ONLY widgets: - label: text: "\U000F0210" align: center text_font: icons text_color: 0xE2E4E5 - button: id: off_button height: 60 width: 60 bg_color: 0x34353E border_width: 2 border_color: 0xE2E4E5 radius: 20 checkable: true checked: bg_color: 0xFF5C57 on_click: then: - lvgl.widget.update: id: fan_button state: checked: false - lvgl.widget.update: id: cool_button state: checked: false - lvgl.widget.update: id: heat_button state: checked: false - climate.control: id: climape mode: 'OFF' widgets: - label: text: "\U000F09DF" text_font: icons align: center text_color: 0xE2E4E5 # Arc Outer Ring only for visual effect - arc: id: arc_ring_outer height: 370 width: 370 y: -20 align: CENTER arc_color: ${color_light_blue} arc_opa: COVER arc_width: 5 knob: bg_color: ${color_white} # Meter ticks and needle - meter: id: meter_ticks hidden: false height: 360 width: 360 y: -20 align: CENTER bg_opa: TRANSP border_width: 0 scales: range_from: 160 # 16°C range_to: 340 # 34°C # Set temperature ticks background ticks: count: 60 length: 18 width: 2 color: ${color_white} indicators: # Curremt temperature needle - line: id: current_temp_needle color: ${color_red} width: 3 r_mod: 10 value: 340 # 34°C Should be the same as the current temperature label # Set temperature ticks - tick_style: id: hvac_temp_ticks start_value: 160 end_value: 240 # 24°C Should be the same as the set temperature label color_start: ${color_green} color_end: ${color_green} width: 2 # Meter inner Ring only for visual effect - obj: id: meter_ring_inner height: 300 width: 300 y: -20 radius: 150 align: CENTER border_width: 12 border_color: ${color_bg} bg_color: ${color_bg_alt} # Set temperature knob, should change with the set temperature ticks - arc: id: hvac_temp_knob hidden: false align: CENTER arc_opa: TRANSP adjustable: true value: 240 # 24°C (24x10) Should be the same as the set temperature needle min_value: 160 max_value: 340 width: 335 height: 335 y: -20 arc_width: 16 change_rate: 30 indicator: arc_opa: TRANSP arc_width: 16 knob: bg_color: ${color_dark_blue} border_width: 4 border_color: ${color_white} on_change: then: - if: condition: lambda: "return id(climape).mode == CLIMATE_MODE_COOL;" then: - climate.control: id: climape target_temperature_high: !lambda |- return x / 10; - if: condition: lambda: "return id(climape).mode == CLIMATE_MODE_FAN_ONLY;" then: - climate.control: id: climape target_temperature_high: !lambda |- return x / 10; - if: condition: lambda: "return id(climape).mode == CLIMATE_MODE_HEAT;" then: - climate.control: id: climape target_temperature_low: !lambda |- return x / 10; # - arc: # id: current_temp_arc # adjustable: true # min_value: 40 # 12°C (* 10 because decimals not supported) # max_value: 95 # 34°C # height: 360 # width: 360 # align: CENTER # arc_width: 20 # indicator: # arc_width: 20 # knob: # width: 30 # value: !lambda |- # auto mode = id(climape).mode; # if (mode == CLIMATE_MODE_COOL) # return id(climape).target_temperature_high * (9.0 / 5.0) + 32; # on_change: # then: # - if: # condition: # lambda: "return id(climape).mode == CLIMATE_MODE_COOL;" # then: # - climate.control: # id: climape # target_temperature_high: !lambda |- # return (x - 32.0) / (9.0 / 5.0); # - if: # condition: # lambda: "return id(climape).mode == CLIMATE_MODE_FAN_ONLY;" # then: # - climate.control: # id: climape # target_temperature_high: !lambda |- # return (x - 32.0) / (9.0 / 5.0); # - if: # condition: # lambda: "return id(climape).mode == CLIMATE_MODE_HEAT;" # then: # - climate.control: # id: climape # target_temperature_low: !lambda |- # return (x - 32.0) / (9.0 / 5.0); - label: id: set_temp_label text_font: h2 text: "Loading..." align: center y: -90 text_color: 0xA5A5A9 - label: id: action_label text_font: h3 text: "Loading..." align: center # x: 145 y: 70 text_color: 0xA5A5A9 - label: id: temp_label text_font: h1 text: "Loading..." align: center y: -10 text_color: 0xE2E4E5