From 5bddada3b83db13c3ce254d274b6f85678f0b2d3 Mon Sep 17 00:00:00 2001 From: Joshua King Date: Sat, 17 Oct 2026 15:04:18 -0400 Subject: [PATCH] adding in resin timer --- esphome/resin-curing-timer.yaml | 361 ++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 esphome/resin-curing-timer.yaml diff --git a/esphome/resin-curing-timer.yaml b/esphome/resin-curing-timer.yaml new file mode 100644 index 0000000..fa24436 --- /dev/null +++ b/esphome/resin-curing-timer.yaml @@ -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( + 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 +