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