adding in resin timer
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
substitutions:
|
||||
device_name: resin-curing-timer
|
||||
outlet_entity: switch.resin_uv_light # Home Assistant UV light outlet.
|
||||
display_model: ESP32-2432S028 # Original ILI9341 CYD; see README for variants.
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: Resin Curing Timer
|
||||
min_version: 2026.9.0
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
|
||||
api:
|
||||
id: ha_api
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
spi:
|
||||
- id: tft_spi
|
||||
clk_pin: GPIO14
|
||||
mosi_pin: GPIO13
|
||||
- id: touch_spi
|
||||
clk_pin: GPIO25
|
||||
mosi_pin: GPIO32
|
||||
miso_pin: GPIO39
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO21
|
||||
id: backlight_output
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: backlight_output
|
||||
name: Backlight
|
||||
id: backlight
|
||||
restore_mode: ALWAYS_ON
|
||||
default_transition_length: 0s
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: tft
|
||||
model: ${display_model}
|
||||
spi_id: tft_spi
|
||||
auto_clear_enabled: false
|
||||
update_interval: never
|
||||
|
||||
touchscreen:
|
||||
- platform: xpt2046
|
||||
id: touch
|
||||
spi_id: touch_spi
|
||||
display: tft
|
||||
cs_pin: GPIO33
|
||||
interrupt_pin: GPIO36
|
||||
threshold: 400
|
||||
# Starting calibration only: verify all four corners on your board.
|
||||
calibration:
|
||||
x_min: 200
|
||||
x_max: 3900
|
||||
y_min: 200
|
||||
y_max: 3900
|
||||
transform:
|
||||
swap_xy: true
|
||||
mirror_x: true
|
||||
on_touch:
|
||||
- logger.log:
|
||||
format: "Touch x=%d y=%d raw_x=%d raw_y=%d"
|
||||
args: [touch.x, touch.y, touch.x_raw, touch.y_raw]
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: timer_state
|
||||
entity_id: timer.resin_curing
|
||||
internal: true
|
||||
- platform: homeassistant
|
||||
id: outlet_state
|
||||
entity_id: ${outlet_entity}
|
||||
internal: true
|
||||
- platform: homeassistant
|
||||
id: timer_duration
|
||||
entity_id: timer.resin_curing
|
||||
attribute: duration
|
||||
internal: true
|
||||
# Keep epoch seconds as text: ESPHome numeric sensors use float32, which
|
||||
# loses minute-level precision for contemporary Unix timestamps.
|
||||
- platform: homeassistant
|
||||
id: deadline
|
||||
entity_id: sensor.resin_curing_deadline
|
||||
internal: true
|
||||
|
||||
script:
|
||||
- id: request_start
|
||||
parameters:
|
||||
minutes: int
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.resin_curing_timer
|
||||
data:
|
||||
controller: ${device_name}
|
||||
command: start
|
||||
minutes: !lambda return to_string(minutes);
|
||||
- id: request_stop
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.resin_curing_timer
|
||||
data:
|
||||
controller: ${device_name}
|
||||
command: stop
|
||||
|
||||
# Only Home Assistant's reported state drives the status and countdown.
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: status_label
|
||||
text: !lambda |-
|
||||
if (!id(ha_api).is_connected_with_state_subscription())
|
||||
return std::string("Home Assistant offline");
|
||||
if (!id(outlet_state).has_state() ||
|
||||
(id(outlet_state).state != "on" && id(outlet_state).state != "off"))
|
||||
return std::string("Outlet unavailable");
|
||||
if (id(outlet_state).state == "on") {
|
||||
if (id(timer_state).state == "active")
|
||||
return std::string("UV light ON | Time remaining");
|
||||
return std::string("UV light ON | No active timer");
|
||||
}
|
||||
return std::string("UV light OFF | Choose a time");
|
||||
- lvgl.label.update:
|
||||
id: countdown_label
|
||||
text: !lambda |-
|
||||
if (!id(ha_api).is_connected_with_state_subscription())
|
||||
return std::string("--:--");
|
||||
if (!id(timer_state).has_state() ||
|
||||
(id(timer_state).state != "active" && id(timer_state).state != "idle"))
|
||||
return std::string("--:--");
|
||||
if (id(timer_state).state == "idle") {
|
||||
if (!id(ha_time).now().is_valid()) return std::string("--:--");
|
||||
return id(ha_time).now().strftime("%H:%M");
|
||||
}
|
||||
const char *raw = id(deadline).state.c_str();
|
||||
char *tail;
|
||||
const double end = strtod(raw, &tail);
|
||||
if (!id(ha_time).now().is_valid() || tail == raw || *tail != '\0' ||
|
||||
!std::isfinite(end) || end <= 0)
|
||||
return std::string("Syncing...");
|
||||
const int remaining = std::max(0, static_cast<int>(
|
||||
end - id(ha_time).now().timestamp));
|
||||
char buffer[16];
|
||||
snprintf(buffer, sizeof(buffer), "%02d:%02d", remaining / 60, remaining % 60);
|
||||
return std::string(buffer);
|
||||
# Highlight only the preset confirmed by Home Assistant, never a local tap.
|
||||
- lvgl.widget.update:
|
||||
id: preset_30
|
||||
state:
|
||||
checked: !lambda |-
|
||||
return id(ha_api).is_connected_with_state_subscription() &&
|
||||
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
|
||||
id(timer_duration).state == "0:30:00";
|
||||
- lvgl.widget.update:
|
||||
id: preset_45
|
||||
state:
|
||||
checked: !lambda |-
|
||||
return id(ha_api).is_connected_with_state_subscription() &&
|
||||
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
|
||||
id(timer_duration).state == "0:45:00";
|
||||
- lvgl.widget.update:
|
||||
id: preset_60
|
||||
state:
|
||||
checked: !lambda |-
|
||||
return id(ha_api).is_connected_with_state_subscription() &&
|
||||
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
|
||||
id(timer_duration).state == "1:00:00";
|
||||
- lvgl.widget.update:
|
||||
id: preset_90
|
||||
state:
|
||||
checked: !lambda |-
|
||||
return id(ha_api).is_connected_with_state_subscription() &&
|
||||
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
|
||||
id(timer_duration).state == "1:30:00";
|
||||
|
||||
lvgl:
|
||||
displays: [tft]
|
||||
rotation: 90
|
||||
touchscreens: [touch]
|
||||
buffer_size: 25%
|
||||
bg_color: 0x1C2638
|
||||
text_color: 0xD6E8F0
|
||||
default_font: montserrat_16
|
||||
theme:
|
||||
button:
|
||||
bg_color: 0x7ED4E6
|
||||
bg_opa: COVER
|
||||
radius: 30
|
||||
width: 60
|
||||
height: 60
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
text_color: 0x102A3A
|
||||
pressed:
|
||||
bg_color: 0xC4F4FA
|
||||
checked:
|
||||
bg_color: 0xF7D264
|
||||
text_color: 0x102A3A
|
||||
pages:
|
||||
- id: timer_page
|
||||
bg_color: 0x1C2638
|
||||
bg_opa: COVER
|
||||
pad_all: 0
|
||||
scrollbar_mode: "OFF"
|
||||
widgets:
|
||||
- label:
|
||||
text: "RESIN CURING"
|
||||
x: 14
|
||||
y: 20
|
||||
text_font: montserrat_14
|
||||
- button:
|
||||
id: stop_button
|
||||
x: 242
|
||||
y: 10
|
||||
width: 64
|
||||
height: 34
|
||||
radius: 17
|
||||
bg_color: 0x343044
|
||||
text_color: 0xF7C0BE
|
||||
pressed:
|
||||
bg_color: 0x793E48
|
||||
widgets:
|
||||
- label:
|
||||
text: "STOP"
|
||||
align: CENTER
|
||||
text_font: montserrat_12
|
||||
on_click:
|
||||
- script.execute: request_stop
|
||||
- label:
|
||||
id: status_label
|
||||
text: "Connecting..."
|
||||
x: 0
|
||||
y: 114
|
||||
width: 320
|
||||
text_align: CENTER
|
||||
text_font: montserrat_12
|
||||
text_color: 0xBED1DF
|
||||
- label:
|
||||
id: countdown_label
|
||||
text: "--:--"
|
||||
x: 0
|
||||
y: 49
|
||||
width: 320
|
||||
text_align: CENTER
|
||||
text_font: montserrat_48
|
||||
text_color: 0xA7EFF5
|
||||
- button:
|
||||
id: preset_30
|
||||
x: 10
|
||||
y: 150
|
||||
widgets:
|
||||
- label:
|
||||
text: "30"
|
||||
align: CENTER
|
||||
text_font: montserrat_24
|
||||
on_click:
|
||||
- script.execute:
|
||||
id: request_start
|
||||
minutes: 30
|
||||
- label:
|
||||
text: "minutes"
|
||||
x: 10
|
||||
y: 217
|
||||
width: 60
|
||||
text_align: CENTER
|
||||
text_font: montserrat_12
|
||||
- button:
|
||||
id: preset_45
|
||||
x: 90
|
||||
y: 150
|
||||
widgets:
|
||||
- label:
|
||||
text: "45"
|
||||
align: CENTER
|
||||
text_font: montserrat_24
|
||||
on_click:
|
||||
- script.execute:
|
||||
id: request_start
|
||||
minutes: 45
|
||||
- label:
|
||||
text: "minutes"
|
||||
x: 90
|
||||
y: 217
|
||||
width: 60
|
||||
text_align: CENTER
|
||||
text_font: montserrat_12
|
||||
- button:
|
||||
id: preset_60
|
||||
x: 170
|
||||
y: 150
|
||||
widgets:
|
||||
- label:
|
||||
text: "60"
|
||||
align: CENTER
|
||||
text_font: montserrat_24
|
||||
on_click:
|
||||
- script.execute:
|
||||
id: request_start
|
||||
minutes: 60
|
||||
- label:
|
||||
text: "minutes"
|
||||
x: 170
|
||||
y: 217
|
||||
width: 60
|
||||
text_align: CENTER
|
||||
text_font: montserrat_12
|
||||
- button:
|
||||
id: preset_90
|
||||
x: 250
|
||||
y: 150
|
||||
widgets:
|
||||
- label:
|
||||
text: "90"
|
||||
align: CENTER
|
||||
text_font: montserrat_24
|
||||
on_click:
|
||||
- script.execute:
|
||||
id: request_start
|
||||
minutes: 90
|
||||
- label:
|
||||
text: "minutes"
|
||||
x: 250
|
||||
y: 217
|
||||
width: 60
|
||||
text_align: CENTER
|
||||
text_font: montserrat_12
|
||||
|
||||
Reference in New Issue
Block a user