First commit
This commit is contained in:
commit
61c37de884
171 files changed
+9201
No files matched your search
@@ -0,0 +1,298 @@
|
||||
# Thermostat - optional carousel screen (base id 7).
|
||||
# A round dial for a Home Assistant climate.* entity (${climate_entity}):
|
||||
# * big centre = the target temperature; turn the KNOB to change it (by the entity's
|
||||
# target_temp_step). The new value is sent to HA ~0.6 s after you stop turning.
|
||||
# * "now ..." = the current room temperature; "RH ..%" = humidity (if the entity has it).
|
||||
# * top label + the target colour show the action: heating (orange) / cooling (blue) /
|
||||
# idle (teal) / off (grey). Bottom chip = the HVAC mode.
|
||||
# * TAP toggles the climate on/off.
|
||||
#
|
||||
# Set ${climate_entity} in your config. Remove this screen by dropping the file from the
|
||||
# package "files:" list.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[7] = true;' # register the thermostat screen
|
||||
|
||||
globals:
|
||||
- id: g_th_cur # current temperature (-127 = no reading yet)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '-127.0'
|
||||
- id: g_th_target # target temperature (synced from HA; edited locally by the knob)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '20.0'
|
||||
- id: g_th_action # hvac_action: heating / cooling / idle / off (may be empty)
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_th_mode # hvac mode (the entity state): off / heat / cool / auto / heat_cool
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_th_hum # current humidity % (-1 = none)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_th_min
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '7.0'
|
||||
- id: g_th_max
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '35.0'
|
||||
- id: g_th_step
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.5'
|
||||
- id: g_th_pending # true = local edit not yet sent; blocks HA from overwriting the target
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_th_lasttap # last g_tap_seq we handled (one tap = one on/off toggle)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
script:
|
||||
# Sends the edited target to HA, but only ~0.6 s after the last knob turn (mode: restart).
|
||||
- id: th_commit
|
||||
mode: restart
|
||||
then:
|
||||
- delay: 600ms
|
||||
- homeassistant.action:
|
||||
action: climate.set_temperature
|
||||
data:
|
||||
entity_id: ${climate_entity}
|
||||
temperature: !lambda 'char b[8]; snprintf(b, sizeof(b), "%.1f", id(g_th_target)); return std::string(b);'
|
||||
- lambda: 'id(g_th_pending) = false;'
|
||||
# Redraws the dial from the state globals.
|
||||
- id: th_render
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: th_target
|
||||
text: !lambda 'char b[12]; snprintf(b, sizeof(b), "%.1f°", id(g_th_target)); return std::string(b);'
|
||||
- lvgl.label.update:
|
||||
id: th_cur
|
||||
text: !lambda |-
|
||||
if (id(g_th_cur) < -100) return std::string("now --");
|
||||
char b[16]; snprintf(b, sizeof(b), "now %.1f°", id(g_th_cur)); return std::string(b);
|
||||
- lvgl.widget.update: { id: th_hum, hidden: !lambda 'return id(g_th_hum) < 0;' }
|
||||
- lvgl.label.update:
|
||||
id: th_hum
|
||||
text: !lambda |-
|
||||
if (id(g_th_hum) < 0) return std::string("");
|
||||
char b[12]; snprintf(b, sizeof(b), "RH %d%%", id(g_th_hum)); return std::string(b);
|
||||
- lvgl.label.update:
|
||||
id: th_mode
|
||||
text: !lambda |-
|
||||
std::string m = id(g_th_mode);
|
||||
if (m == "heat_cool") return std::string("Auto");
|
||||
if (m.empty()) return std::string("-");
|
||||
if (m[0] >= 'a' && m[0] <= 'z') m[0] -= 32; return m;
|
||||
- lvgl.arc.update:
|
||||
id: th_arc
|
||||
value: !lambda |-
|
||||
float r = id(g_th_max) - id(g_th_min);
|
||||
if (r <= 0) return 0.0f;
|
||||
float p = 100.0f * (id(g_th_target) - id(g_th_min)) / r;
|
||||
if (p < 0) p = 0; if (p > 100) p = 100;
|
||||
return p;
|
||||
- lvgl.label.update:
|
||||
id: th_action
|
||||
text: !lambda |-
|
||||
std::string m = id(g_th_mode), a = id(g_th_action);
|
||||
if (m == "off" || m.empty()) return std::string("OFF");
|
||||
if (a.find("heat") != std::string::npos || (a.empty() && m == "heat")) return std::string("HEATING");
|
||||
if (a.find("cool") != std::string::npos || (a.empty() && m == "cool")) return std::string("COOLING");
|
||||
if (a == "idle") return std::string("IDLE");
|
||||
if (!a.empty()) { if (a[0] >= 'a' && a[0] <= 'z') a[0] -= 32; return a; }
|
||||
if (m[0] >= 'a' && m[0] <= 'z') m[0] -= 32; return m;
|
||||
# accent colour for the action label + target by heat/cool/off/idle
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(g_th_mode) != "off" && !id(g_th_mode).empty() && (id(g_th_action).find("heat") != std::string::npos || (id(g_th_action).empty() && id(g_th_mode) == "heat"));'
|
||||
then:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0xFF6B30 }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0xFF6B30 }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0xFF6B30 } }
|
||||
else:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(g_th_mode) != "off" && !id(g_th_mode).empty() && (id(g_th_action).find("cool") != std::string::npos || (id(g_th_action).empty() && id(g_th_mode) == "cool"));'
|
||||
then:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0x3FA9D6 }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0x3FA9D6 }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x3FA9D6 } }
|
||||
else:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_th_mode) == "off" || id(g_th_mode).empty();' }
|
||||
then:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0x6F9A92 }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0x6F9A92 }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x44506A } }
|
||||
else:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0x9FC7BE }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0xFFFFFF }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x2FE6CF } }
|
||||
- lambda: |- # position the target head-dot on the gauge
|
||||
float r = id(g_th_max) - id(g_th_min);
|
||||
float p = (r > 0) ? 100.0f * (id(g_th_target) - id(g_th_min)) / r : 0.0f;
|
||||
if (p < 0) p = 0; if (p > 100) p = 100;
|
||||
float a = (135.0f + p / 100.0f * 270.0f) * 0.01745329f;
|
||||
int x = 180 + (int) lroundf(144.0f * cosf(a)) - 9;
|
||||
int y = 180 + (int) lroundf(144.0f * sinf(a)) - 9;
|
||||
lv_obj_set_pos(id(th_dot), x, y);
|
||||
|
||||
sensor:
|
||||
- platform: homeassistant
|
||||
id: th_cur_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: current_temperature
|
||||
on_value:
|
||||
- lambda: 'id(g_th_cur) = x;'
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_tgt_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: temperature
|
||||
on_value:
|
||||
- lambda: 'if (!id(g_th_pending)) id(g_th_target) = x;' # don't clobber a value being edited
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_hum_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: current_humidity
|
||||
on_value:
|
||||
- lambda: 'id(g_th_hum) = (int) roundf(x);'
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_min_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: min_temp
|
||||
on_value: [ lambda: 'id(g_th_min) = x;' ]
|
||||
- platform: homeassistant
|
||||
id: th_max_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: max_temp
|
||||
on_value: [ lambda: 'id(g_th_max) = x;' ]
|
||||
- platform: homeassistant
|
||||
id: th_step_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: target_temp_step
|
||||
on_value: [ lambda: 'if (x > 0) id(g_th_step) = x;' ]
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: th_mode_s
|
||||
entity_id: ${climate_entity} # the entity state IS the hvac mode
|
||||
on_value:
|
||||
- lambda: 'id(g_th_mode) = x;'
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_action_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: hvac_action
|
||||
on_value:
|
||||
- lambda: 'id(g_th_action) = x;'
|
||||
- script.execute: th_render
|
||||
|
||||
interval:
|
||||
# Carousel nav: when selected, show the page and take the knob.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==7;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_knob_capture) = true; id(g_th_lasttap) = id(g_tap_seq);'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: th_render
|
||||
# Knob (adjust target, debounced commit) + tap (on/off) while this screen is on top.
|
||||
- interval: 120ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==7 && id(g_knob_delta) != 0;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_th_target) += id(g_knob_delta) * id(g_th_step);
|
||||
id(g_knob_delta) = 0;
|
||||
if (id(g_th_target) < id(g_th_min)) id(g_th_target) = id(g_th_min);
|
||||
if (id(g_th_target) > id(g_th_max)) id(g_th_target) = id(g_th_max);
|
||||
id(g_th_pending) = true;
|
||||
- script.execute: th_commit
|
||||
- script.execute: th_render
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==7 && id(g_tap_seq) != id(g_th_lasttap);' }
|
||||
then:
|
||||
- lambda: 'id(g_th_lasttap) = id(g_tap_seq);'
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_th_mode) == "off";' }
|
||||
then:
|
||||
- homeassistant.action:
|
||||
action: climate.turn_on
|
||||
data:
|
||||
entity_id: ${climate_entity}
|
||||
else:
|
||||
- homeassistant.action:
|
||||
action: climate.turn_off
|
||||
data:
|
||||
entity_id: ${climate_entity}
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_thermostat
|
||||
bg_color: 0x0B1020
|
||||
widgets:
|
||||
- obj: # faint outer ring for depth
|
||||
id: th_ring_out
|
||||
align: CENTER
|
||||
width: 332
|
||||
height: 332
|
||||
radius: 166
|
||||
bg_opa: TRANSP
|
||||
border_width: 2
|
||||
border_color: 0x1B2740
|
||||
border_opa: 70%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- arc:
|
||||
id: th_arc
|
||||
align: CENTER
|
||||
width: 300
|
||||
height: 300
|
||||
arc_width: 12
|
||||
adjustable: false
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
value: 50
|
||||
start_angle: 135
|
||||
end_angle: 45
|
||||
arc_color: 0x21304A
|
||||
indicator:
|
||||
arc_color: 0x2FE6CF
|
||||
arc_width: 12
|
||||
- obj: # head-dot: white bead marking the target on the gauge (positioned in th_render)
|
||||
id: th_dot
|
||||
x: 0
|
||||
y: 0
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
shadow_color: 0xFFFFFF
|
||||
shadow_width: 10
|
||||
shadow_opa: 55%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- label: { id: th_action, align: CENTER, y: -86, text: "", text_font: font_artist, text_color: 0x9FC7BE }
|
||||
- label: { id: th_target, align: CENTER, y: -14, text: "--", text_font: montserrat_48, text_color: 0xFFFFFF }
|
||||
- label: { id: th_cur, align: CENTER, y: 30, text: "now --", text_font: font_med, text_color: 0x9FC7BE }
|
||||
- label: { id: th_hum, align: CENTER, y: 58, text: "", text_font: font_artist, text_color: 0x3FA9D6, hidden: true }
|
||||
- label: { id: th_mode, align: CENTER, y: 92, text: "", text_font: font_artist, text_color: 0xCFEAE4 }
|
||||
Reference in new issue
Block a user