Files
zorruno-homeassistant/esphome/esp-poollightpower2.yaml
T
2026-01-24 14:06:52 +13:00

1059 lines
40 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
##########################################################################################
##########################################################################################
# POOL LIGHT POWER AND TIMER 2
# Controlled by a Athom Smart Plug V2
#
# v4.0 2025-11-04 Changed to Athom V2
# V3.0 2025-09-02 Pool Light mode controller fixes; Mode select forces Operation=ON;
# added Mode 0=OFF; select shows OFF when relay off; enforce 7s min OFF window
# V2.4 2025-06-15 Changed back to an Athom V1 (esp8266)
# V2.3 2025-06-15 Changed to an Athom V3 (esp32)
# V2.2 2025-06-14 Fixes to offline time when sntp/network unavailable
# V2.1 2025-06-12 Added select and button to chose modes, added countdown & startup to boost
# V2.0 2025-06-05 YAML Tidyups
#
# INSTRUCTIONS
# - It allows the device to work in a standalone timer style operation
# - The timer has a morning and evening time (but no weekday/weekend settings)
# - Default values are set, but changed values are remembered in flash
# - It uses SNTP for time setting (but obviously only if wifi & networking are working)
# - It will default to an internal timer if no wifi. To reset internal timer, reboot the device at 12pm (noon)
# - If on a network and there is a MQTT server, you can set the on/off times via MQTT (See below commands)
# - You can set 4 modes ON/OFF/TIMER/BOOST via MQTT. Setting BOOST gives you a oneshot operation
# - Any new timer times set via MQTT will be remembered though a reboot
# - On startup, or a reboot, the device will always turn on for the BOOST Duration (BOOST mode, default 2 hours)
# - TIMER mode will always be switched on after BOOST mode is complete
# - Home Assistant entities are set so that BOOST mode can be pressed with a button and other modes selectable with a dropdown
# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 30 seconds (with ~2 secs in between to allow it to boot)
#
# MQTT Commands
# Values will be set in place on the update_interval time, not immediately
# Use 00:00 in 24hr format for time setting. (Note there is no weekday/weekend setting)
# mqtt_timer_topic/morning-on/06:00 : Time device will go on
# mqtt_timer_topic/morning-off/08:00 : Time device will go off
# mqtt_timer_topic/evening-on/09:00 : Time device will go on
# mqtt_timer_topic/evening-off/00:00 : Time device will go off
# mqtt_timer_topic/boost-time/0000 : Time in minutes device will temporarily go on for (1-1439)
# mqtt_timer_topic/operation/ON : Device permanently on
# mqtt_timer_topic/operation/OFF : Device permanently off
# mqtt_timer_topic/operation/TIMER : Device will obey timer settings
# mqtt_timer_topic/operation/BOOST : Turn on for (boost_duration) minutes then BOOST (also on startup)
#
# operation_mode:
# 0 = OFF
# 1 = ON
# 2 = TIMER
# 3 = BOOST
#
##########################################################################################
##########################################################################################
##########################################################################################
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
##########################################################################################
substitutions:
# Device Naming
device_name: "esp-poollightpower2"
friendly_name: "Pool Light Power"
description_comment: "Pool Light Power :: Athom Smart Plug Power V2"
device_area: "Outside" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
# Project Naming
project_name: "Athom Technology.Smart Plug V2" # Project Details
project_version: "v3.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
# Passwords
api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names
ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-poollightpower2_ip
# Device Settings
log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "10s" # update time for for general sensors etc
# Device Settings
relay_icon: "mdi:power-socket-au"
current_limit : "10" # Current Limit in Amps. AU Plug = 10. IL, BR, EU, UK, US Plug = 16.
mqtt_timer_topic: "viewroad-commands/poollight-timer" # Topics you will use to change stuff
boost_duration_default: "180" # Minutes to stay ON in BOOST mode before reverting to TIMER
morning_on_default: "450" # Default in minutes from midnight. Default 07:30 => 450
morning_off_default: "450" # Default in minutes from midnight. Default 07:30 => 450 (same as ON as no need for morning schedule)
evening_on_default: "1140" # Default in minutes from midnight. Default 19:00 => 1140
evening_off_default: "1350" # Default in minutes from midnight. Default 22:30 => 1350 => 1440 is midnight
##############################################################################
# POOL LIGHT MODE CONTROL (new)
##############################################################################
max_modes: "16" # total number of modes in the cycle
mode_pulse_off: "400ms" # short OFF between quick pulses
mode_pulse_on: "400ms" # short ON after each quick pulse
mode_reset_off: "3s" # long OFF to force hardware reset to mode 1
mode_reset_window_ms: "3000" # treat OFF>=this ms as a reset-to-1 when turning back ON
mode_prime_on: "800ms" # if relay is OFF when a color is selected, turn ON for this long before pulsing
# Ignored modes bitmask: bit (mode_index-1). For 4 and 5 ignored -> (1<<3)|(1<<4) = 24.
ignored_modes_mask: "8288" # Ignore 6(32), 7(64), 14(8192) :: 32 + 64 + 8192 = 8288
##########################################################################################
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
##########################################################################################
packages:
##################################################
# MANDATORY packages (won't compile without them!)
##################################################
#### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
common_wifi: !include
file: common/network_common.yaml
vars:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
##################################################
# OPTIONAL packages (comment if you don't want them)
##################################################
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
file: common/api_common.yaml
#file: common/api_common_noencryption.yaml
vars:
local_api_key: "${api_key}"
#### WEB PORTAL ####
#common_webportal: !include
# file: common/webportal_common.yaml
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### SNTP (Only use if you want/need accurate timeclocks) ####
common_sntp: !include
file: common/sntp_common.yaml
#### DIAGNOSTICS Lite Sensors ####
common_lite_sensors: !include
file: common/sensors_common_lite.yaml
#### DIAGNOSTICS General Sensors ####
common_general_sensors: !include
file: common/sensors_common.yaml
#### DEBUGGING Sensors ####
# A count of various resets
#common_resetcount_debugging: !include
# file: common/resetcount_common.yaml
# Device Specific included packages
common_athompowermonV2: !include
file: common/athompowermonv2_common.yaml
vars:
local_friendly_name: "${friendly_name}"
#local_current_limit: "${current_limit}"
sensor_update_interval: "${update_interval}" # e.g. "10s"
hide_energy_sensor: "false" # or "true" if you want Energy hidden
relay_restore_mode: "RESTORE_DEFAULT_OFF" # matches what you already use
power_plug_type: "${relay_icon}" # e.g. "mdi:power-socket-au"
##########################################################################################
# ESPHome
# https://esphome.io/components/esphome.html
##########################################################################################
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
comment: "${description_comment}" #Appears on the esphome page in HA
area: "${device_area}"
name_add_mac_suffix: False
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
#platformio_options:
# build_unflags:
# - -std=gnu++20
# - -std=gnu++2a
# build_flags:
# - -std=gnu++11
# - -Os
# - -Wl,--gc-sections
# - -fno-exceptions
# - -fno-rtti
##########################################################################################
# ESP Platform and Framework
# https://esphome.io/components/esp32.html
##########################################################################################
esp8266:
board: esp8285
restore_from_flash: true # mainly for calculating cumulative energy, but not that important here
#framework:
# version: 2.7.4
preferences:
flash_write_interval: 10min
mdns:
disabled: true # binary size saving
##########################################################################################
# ESPHome Logging Enable
# https://esphome.io/components/logger.html
##########################################################################################
logger:
level: "${log_level}" #INFO Level suggested, or DEBUG for testing
baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM)
#esp8266_store_log_strings_in_flash: false
#tx_buffer_size: 64
##########################################################################################
# Global Variables for use in automations etc
# https://esphome.io/guides/automations.html?highlight=globals#global-variables
##########################################################################################
globals:
# Tracks the time (in seconds from midnight) at the previous boot
- id: last_boot_time_s
type: int
restore_value: true
initial_value: "0"
# Counts how many consecutive boots have occurred (within X seconds)
- id: boot_count
type: int
restore_value: true
initial_value: "0"
# Morning On time (minutes from midnight),
- id: morning_on
type: int
restore_value: true
initial_value: "${morning_on_default}"
# Morning Off time (minutes from midnight),
- id: morning_off
type: int
restore_value: true
initial_value: "${morning_off_default}"
# Evening On time (minutes from midnight),
- id: evening_on
type: int
restore_value: true
initial_value: "${evening_on_default}"
# Evening Off time (minutes from midnight),
- id: evening_off
type: int
restore_value: true
initial_value: "${evening_off_default}"
# Boost Duration (minutes),
- id: boost_duration
type: int
restore_value: true
initial_value: "${boost_duration_default}"
####################################################
# operation_mode:
# 0 = OFF
# 1 = ON
# 2 = TIMER
# 3 = BOOST
####################################################
- id: operation_mode
type: int
restore_value: true
initial_value: "2"
####################################################
# boost_timer: counts minutes in BOOST mode
# After 'boost_duration' minutes, revert to TIMER.
####################################################
- id: boost_timer
type: int
restore_value: true
initial_value: "0"
##############################################################################
# POOL LIGHT MODE CONTROL GLOBALS (new)
##############################################################################
# Current assumed pool-light mode (0..max_modes). 0 means relay OFF.
- id: current_mode
type: int
restore_value: false
initial_value: "1"
# Total modes in the cycle (edit in substitutions).
- id: max_modes
type: int
restore_value: false
initial_value: "${max_modes}"
# Flag while we are actively pulsing to change modes (prevents scheduler interference).
- id: mode_changing
type: bool
restore_value: false
initial_value: "false"
# Timestamp of last relay off (for delta measurement).
- id: last_off_ms
type: uint32_t
restore_value: false
initial_value: "0"
# Millisecond threshold that separates "quick pulse" vs "reset to mode 1".
- id: reset_window_ms
type: int
restore_value: false
initial_value: "${mode_reset_window_ms}"
# Desired target mode for the cycle script.
- id: desired_mode
type: int
restore_value: false
initial_value: "1"
# Bitmask of ignored modes (1..max_modes). Example: ignore 4 & 5 -> 24.
- id: ignored_mask
type: int
restore_value: false
initial_value: "${ignored_modes_mask}"
# Internal: whether a recheck is already scheduled for min OFF enforcement.
- id: recheck_pending
type: bool
restore_value: false
initial_value: "false"
##########################################################################################
# Text Sensors
# https://esphome.io/components/text_sensor/index.html
##########################################################################################
text_sensor:
############################
# MQTT Subscriptions
############################
####################################################
# Subscribe to the Morning On time, format "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Morning On Time Setting"
id: morning_on_topic
topic: "${mqtt_timer_topic}/morning-on" # Stored in the format HH:MM
internal: true
on_value:
then:
- lambda: |-
// Expect "HH:MM" => total length = 5, with ':'
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str()); // "HH"
int minute = atoi(x.substr(3, 2).c_str()); // "MM"
id(morning_on) = hour * 60 + minute;
ESP_LOGI("timer","Received new Morning On: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Morning On format: %s", x.c_str());
}
####################################################
# Morning Off time => "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Morning Off Time Setting"
id: morning_off_topic
topic: "${mqtt_timer_topic}/morning-off" # Stored in the format HH:MM
internal: true # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str());
int minute = atoi(x.substr(3, 2).c_str());
id(morning_off) = hour * 60 + minute;
ESP_LOGI("timer","Received new Morning Off: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Morning Off format: %s", x.c_str());
}
####################################################
# Evening On time => "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Evening On Time Setting"
id: evening_on_topic
topic: "${mqtt_timer_topic}/evening-on" # Stored in the format HH:MM
internal: true # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str());
int minute = atoi(x.substr(3, 2).c_str());
id(evening_on) = hour * 60 + minute;
ESP_LOGI("timer","Received new Evening On: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Evening On format: %s", x.c_str());
}
####################################################
# Evening Off time => "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Evening Off Time Setting"
id: evening_off_topic
topic: "${mqtt_timer_topic}/evening-off" # Stored in the format HH:MM
internal: true # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str());
int minute = atoi(x.substr(3, 2).c_str());
id(evening_off) = hour * 60 + minute;
ESP_LOGI("timer","Received new Evening Off: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Evening Off format: %s", x.c_str());
}
####################################################
# Boost duration => 1 - 1439
####################################################
- platform: mqtt_subscribe
name: "Boost Duration"
id: boost_time_topic
topic: "${mqtt_timer_topic}/boost-time" # Stored as an integer from 1-1439
internal: true # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
// parse as integer
char *endptr;
long v = strtol(x.c_str(), &endptr, 10);
// invalid if nothing parsed, trailing chars, or out of 01439
if (endptr == x.c_str() || *endptr != '\0' || v < 0 || v > 1439) {
ESP_LOGE("boost_time", "Invalid boost_time '%s'", x.c_str());
} else {
id(boost_duration) = static_cast<int>(v);
}
####################################################
# Subscribe to operation mode:
# OFF, ON, TIMER, BOOST
####################################################
# MQTT subscription: set mode, then immediately re-evaluate relay
- platform: mqtt_subscribe
id: timer_operation_mode_topic
topic: "${mqtt_timer_topic}/operation"
internal: true # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (strcasecmp(x.c_str(), "TIMER") == 0) {
id(operation_mode) = 2;
ESP_LOGI("timer","Operation mode set to TIMER");
} else if (strcasecmp(x.c_str(), "ON") == 0) {
id(operation_mode) = 1;
ESP_LOGI("timer","Operation mode set to ON");
} else if (strcasecmp(x.c_str(), "OFF") == 0) {
id(operation_mode) = 0;
ESP_LOGI("timer","Operation mode set to OFF");
} else if (strcasecmp(x.c_str(), "BOOST") == 0) {
id(operation_mode) = 3;
id(boost_timer) = 0;
ESP_LOGI("timer","Operation mode set to BOOST");
} else {
ESP_LOGW("timer","Invalid operation mode: %s", x.c_str());
}
- script.execute: evaluate_relay_state
######################################################
# Expose the current operation mode (OFF, ON, TIMER, BOOST)
######################################################
- platform: template
name: "Operation Mode State"
lambda: |-
// 0=OFF, 1=ON, 2=TIMER, 3=BOOST
switch (id(operation_mode)) {
case 0: return {"OFF"};
case 1: return {"ON"};
case 2: return {"TIMER"};
case 3: return {"BOOST"};
default: return {"UNKNOWN"};
}
update_interval: 5s
######################################################
# Expose the "Morning On" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Morning On Time"
lambda: |-
int hour = id(morning_on) / 60;
int minute = id(morning_on) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Morning Off" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Morning Off Time"
lambda: |-
int hour = id(morning_off) / 60;
int minute = id(morning_off) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Evening On" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Evening On Time"
lambda: |-
int hour = id(evening_on) / 60;
int minute = id(evening_on) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Evening Off" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Evening Off Time"
lambda: |-
int hour = id(evening_off) / 60;
int minute = id(evening_off) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
##########################################################################################
#binary_sensor:
# - platform: gpio
# pin:
# number: GPIO03
# mode: INPUT_PULLUP
# inverted: true
# name: "Power Button"
# id: power_button
# filters:
# - delayed_on: 20ms
# on_click:
# - min_length: 20ms
# max_length: 500ms
# then:
# - lambda: |-
# if (id(relay).state) {
# // Relay is ON: turn it OFF and set mode to 2 (TIMER)
# id(relay).turn_off();
# id(operation_mode) = 2;
# } else {
# // Relay is OFF: turn it ON and set mode to 3 (BOOST)
# id(relay).turn_on();
# id(operation_mode) = 3;
# }
binary_sensor:
- platform: template
id: relay_status
name: "Relay Status"
lambda: |-
return id(relay).state;
# Mirror the package relay state and run your previous hooks
- platform: template
id: relay_shadow
internal: true
lambda: |-
return id(relay).state;
on_press:
- lambda: |-
// Mirrors your former switch.on_turn_on
if (id(mode_changing)) {
return;
}
uint32_t delta = millis() - id(last_off_ms);
if (delta >= (uint32_t) id(reset_window_ms)) {
id(current_mode) = 1;
} else {
id(current_mode)++;
if (id(current_mode) > id(max_modes)) id(current_mode) = 1;
}
on_release:
- lambda: |-
// Mirrors your former switch.on_turn_off
id(last_off_ms) = millis();
if (!id(mode_changing)) id(current_mode) = 0;
##########################################################################################
# Sensors
# https://esphome.io/components/text_sensor/index.html
##########################################################################################
sensor:
- platform: template
name: "Timeclock: Boost Duration"
id: boost_duration_time
unit_of_measurement: "mins"
accuracy_decimals: "0"
update_interval: "${update_interval}"
lambda: |-
return id(boost_duration);
- platform: template
name: "Mins from Midnight"
id: mins_from_midnight
unit_of_measurement: "mins"
accuracy_decimals: "0"
update_interval: "${update_interval}"
internal: true # No need to show this in Home Assistant
lambda: |-
return id(current_mins);
# A value in mins if a timer is running showing how many mins left
- platform: template
name: "Timer Minutes Remaining"
id: timer_minutes_remaining
unit_of_measurement: "Mins"
update_interval: 5s
accuracy_decimals: "0"
lambda: |-
// always zero if relay is off
if (!id(relay).state) {
return 0;
}
int rem = 0;
// only calculate for mode 2 (scheduled) or mode 3 (BOOST)
if (id(operation_mode) == 2) {
int a = id(morning_off) - id(current_mins);
int b = id(evening_off) - id(current_mins);
// if a is negative, use b; otherwise pick the smaller of a or b
rem = (a < 0) ? b : (a < b ? a : b);
}
else if (id(operation_mode) == 3) {
rem = id(boost_duration) - id(boost_timer);
}
// never return negative
return rem > 0 ? rem : 0;
#################################################################################################
# SWITCH COMPONENT
# https://esphome.io/components/switch/
#################################################################################################
#switch:
# - platform: gpio
# name: "Power Output"
# pin: GPIO14
# id: relay
# restore_mode: RESTORE_DEFAULT_OFF # Ensures the relay is restored (or off) at boot
# #internal: true # Hides the switch from Home Assistant
# icon: "${relay_icon}"
# # POOL LIGHT MODE CONTROL: detect quick vs long off periods and keep current_mode in sync
# on_turn_off:
# - lambda: |-
# id(last_off_ms) = millis();
# if (!id(mode_changing)) id(current_mode) = 0; // reflect OFF in select when off normally
# on_turn_on:
# - lambda: |-
# if (id(mode_changing)) {
# return;
# }
# uint32_t delta = millis() - id(last_off_ms);
# if (delta >= (uint32_t)id(reset_window_ms)) {
# id(current_mode) = 1;
# } else {
# id(current_mode)++;
# if (id(current_mode) > id(max_modes)) id(current_mode) = 1; // keep internal index sane
# }
#################################################################################################
# BUTTON COMPONENT
# https://esphome.io/components/button/index.html
#################################################################################################
button:
- platform: template
name: "Boost now"
id: boost_button
icon: "mdi:play-circle-outline"
on_press:
# 1) reset BOOST timer and set mode
- lambda: |-
id(boost_timer) = 0;
id(operation_mode) = 3;
# 2) immediately re-evaluate relay state
- script.execute: evaluate_relay_state
- platform: template
name: "Default timer settings"
id: default_timer_settings_button
icon: "mdi:restore"
on_press:
- lambda: |-
// Restore all timing globals to their YAML defaults
id(morning_on) = ${morning_on_default};
id(morning_off) = ${morning_off_default};
id(evening_on) = ${evening_on_default};
id(evening_off) = ${evening_off_default};
id(boost_duration)= ${boost_duration_default};
// Reset mode to TIMER and clear any running boost
id(operation_mode)= 2;
id(boost_timer) = 0;
ESP_LOGI("timer","Default timer settings applied");
- script.execute: evaluate_relay_state
#################################################################################################
# SELECT COMPONENT
# https://esphome.io/components/select/index.html
#################################################################################################
select:
- platform: template
name: "Operation Mode"
id: operation_mode_select
update_interval: 5s
options:
- "OFF"
- "ON"
- "TIMER"
- "BOOST"
# show the current mode
lambda: |-
switch (id(operation_mode)) {
case 1: return std::string("ON");
case 2: return std::string("TIMER");
case 3: return std::string("BOOST");
default: return std::string("OFF");
}
# when changed in HA, set mode & re-evaluate
set_action:
- lambda: |-
if (x == "OFF") { id(operation_mode) = 0; }
else if (x == "ON") { id(operation_mode) = 1; }
else if (x == "TIMER") { id(operation_mode) = 2; }
else { // BOOST
id(boost_timer) = 0;
id(operation_mode) = 3;
}
- script.execute: evaluate_relay_state
##############################################################################
# POOL LIGHT MODE SELECT (new)
# 0=OFF -> immediately turns relay OFF and sets Operation Mode = TIMER.
# Selecting a color (>=1) forces Operation Mode = ON.
# If target < current, do a long reset to mode 1, then pulse forward.
# Ignored modes are hidden here but still counted by pulses.
##############################################################################
##############################################################################
# POOL LIGHT MODE SELECT (updated display logic)
# 0=OFF -> immediately turns relay OFF and sets Operation Mode = TIMER.
# Selecting a color (>=1) forces Operation Mode = ON.
# If target < current, do a long reset to mode 1, then pulse forward.
# Ignored modes are hidden here but still counted by pulses.
##############################################################################
- platform: template
name: "Pool Light Mode"
id: pool_light_mode_select
update_interval: 250ms
# Options include all 1..16 (no ignored modes currently) plus 0=OFF
options:
- "0= OFF"
- "1= Dark Blue"
- "2= Red"
- "3= Green"
- "4= Purple"
- "5= Aqua"
#- "6= Lime"
#- "7= Cool White"
- "8= Speed Up"
- "9= Transformer"
- "10= Blurple"
- "11= Redlime"
- "12= Greenqua"
- "13= Bluequa"
#- "14= Grelime"
- "15= Redurple"
- "16= RGB Mad"
# Show OFF only when not in a color-change sequence.
# While changing, reflect the in-flight/current mode (never 0).
lambda: |-
auto label_for = [](int m) -> std::string {
switch (m) {
case 0: return "0= OFF";
case 1: return "1= Dark Blue";
case 2: return "2= Red";
case 3: return "3= Green";
case 4: return "4= Purple";
case 5: return "5= Aqua";
case 6: return "6= Lime";
case 7: return "7= Cool White";
case 8: return "8= Speed Up";
case 9: return "9= Transformer";
case 10: return "10= Blurple";
case 11: return "11= Redlime";
case 12: return "12= Greenqua";
case 13: return "13= Bluequa";
case 14: return "14= Grelime";
case 15: return "15= Redurple";
case 16: return "16= RGB Mad";
default: return "1= Dark Blue";
}
};
auto is_ignored = [&](int m) -> bool {
if (m < 1 || m > id(max_modes)) return true;
int bit = 1 << (m - 1);
return (id(ignored_mask) & bit) != 0;
};
// While stepping (mode_changing), don't show OFF even if relay is briefly OFF.
if (id(mode_changing)) {
int cm = id(current_mode);
if (cm <= 0) cm = 1; // during long reset, assume Mode 1 is next
// Skip ignored modes for display if ever configured later
if (!is_ignored(cm)) return label_for(cm);
// find next non-ignored just for display robustness
for (int m = cm + 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
for (int m = 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
return label_for(1);
}
// Normal (not changing): show OFF if relay is actually OFF, else show current mode
if (!id(relay).state) return std::string("0= OFF");
int cm = id(current_mode);
if (cm == 0) return std::string("0= OFF");
if (!is_ignored(cm)) return label_for(cm);
for (int m = cm + 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
for (int m = 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
return label_for(1);
set_action:
# First, set flags based on selection
- lambda: |-
int target = atoi(x.c_str());
if (target < 0) target = 0;
if (target > id(max_modes)) target = id(max_modes);
if (target == 0) {
// MODE 0: force everything OFF; UI goes back to TIMER control
id(mode_changing) = false;
id(recheck_pending) = false;
id(operation_mode) = 2; // TIMER
id(current_mode) = 0; // reflect OFF in dropdown
} else {
// defensive: ignore choosing an ignored mode (options already hide them)
int bit = 1 << (target - 1);
if ((id(ignored_mask) & bit) != 0) {
ESP_LOGW("mode","Selected target %d is ignored; ignoring request", target);
return;
}
// Color selection: force ON mode and start changer
id(operation_mode) = 1;
id(desired_mode) = target;
id(mode_changing) = true;
}
# If OFF was selected, hard-stop any running scripts and cut power
- if:
condition:
lambda: 'return atoi(x.c_str()) == 0;'
then:
- script.stop: change_to_desired_mode
- script.stop: reset_to_mode1
- script.stop: enforce_off_guard
- switch.turn_off: relay
else:
- script.execute: change_to_desired_mode
#################################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
#################################################################################################
script:
# Core: evaluate and drive the relay (with enforced min OFF window)
- id: evaluate_relay_state
then:
- lambda: |-
if (id(mode_changing)) {
// do not fight the pulser
return;
}
// Determine target state
bool target_on = false;
int mode = id(operation_mode);
if (mode == 3) { // BOOST
target_on = true;
} else if (mode == 0) { // OFF
target_on = false;
} else if (mode == 1) { // ON
target_on = true;
} else if (mode == 2) { // TIMER
bool should_on = false;
if (id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off))
should_on = true;
if (id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off))
should_on = true;
target_on = should_on;
}
// Enforce minimum OFF time when turning ON normally (not during color pulses)
if (target_on) {
uint32_t delta = millis() - id(last_off_ms);
if (delta < (uint32_t) id(reset_window_ms)) {
// Still inside OFF window: wait; schedule a recheck if not already
if (!id(recheck_pending)) {
id(enforce_off_guard).execute();
}
return;
} else {
id(relay).turn_on();
return;
}
} else {
id(relay).turn_off();
return;
}
# Poll until OFF window elapsed, then re-evaluate (does not toggle the relay itself)
- id: enforce_off_guard
mode: restart
then:
- lambda: |-
id(recheck_pending) = true;
- while:
condition:
lambda: |-
if (id(mode_changing)) return false; // abort if color change starts
uint32_t delta = millis() - id(last_off_ms);
return delta < (uint32_t) id(reset_window_ms);
then:
- delay: 200ms
- lambda: |-
id(recheck_pending) = false;
- script.execute: evaluate_relay_state
# Long reset to Mode 1
- id: reset_to_mode1
mode: restart
then:
- switch.turn_off: relay
- delay: ${mode_reset_off}
- switch.turn_on: relay
- delay: ${mode_pulse_on}
- lambda: |-
id(current_mode) = 1;
# Change to desired_mode, robust from OFF and without wrap on lower targets
# Change to desired_mode, robust from OFF and without wrap on lower targets
- id: change_to_desired_mode
mode: restart
then:
# If relay is currently OFF, establish a known starting point
- if:
condition:
lambda: 'return !id(relay).state;'
then:
- lambda: |-
uint32_t delta = millis() - id(last_off_ms);
// If OFF < reset window, enforce a true reset first for predictable start
if (delta < (uint32_t) id(reset_window_ms)) {
id(reset_to_mode1).execute();
}
- if:
condition:
lambda: 'return !id(relay).state;'
then:
- switch.turn_on: relay
- delay: ${mode_prime_on}
- lambda: |-
// If we turned ON after ≥ reset window, hardware is at Mode 1
if ((millis() - id(last_off_ms)) >= (uint32_t) id(reset_window_ms)) {
id(current_mode) = 1;
}
# If target is below current, do a long reset to Mode 1, then proceed
- if:
condition:
lambda: |-
return id(desired_mode) < id(current_mode);
then:
- script.execute: reset_to_mode1
- script.wait: reset_to_mode1
# If already exactly at target, ensure ON and finish
- if:
condition:
lambda: 'return id(desired_mode) == id(current_mode);'
then:
- switch.turn_on: relay
- lambda: |-
id(mode_changing) = false;
# Step forward until we reach desired (ignored modes still counted)
- while:
condition:
lambda: |-
// Allow immediate cancellation if OFF is selected mid-flight
return id(mode_changing) && (id(current_mode) < id(desired_mode));
then:
- switch.turn_off: relay
- delay: ${mode_pulse_off}
- switch.turn_on: relay
- delay: ${mode_pulse_on}
- lambda: |-
id(current_mode)++;
# End: make sure we leave the light ON and clear the guard
- switch.turn_on: relay
- lambda: |-
id(mode_changing) = false;
#################################################################################################
# INTERVAL COMPONENT
# https://esphome.io/components/interval.html
#################################################################################################
# Interval: bumps time (even if no SNTP), then calls the script to evaluate relay state
interval:
- interval: "1min"
then:
- lambda: |-
// — update current_mins via SNTP or fallback
if (!id(sntp_time).now().is_valid()) {
id(current_mins)++;
if (id(current_mins) >= 1440) id(current_mins) = 0;
} else {
auto now = id(sntp_time).now();
id(current_mins) = now.hour * 60 + now.minute;
}
// — if in BOOST, advance boost_timer and expire when done
if (id(operation_mode) == 3) {
id(boost_timer)++;
if (id(boost_timer) >= id(boost_duration)) {
id(operation_mode) = 2;
//id(mqtt_client).publish("${mqtt_timer_topic}/operation", "TIMER");
}
}
- script.execute: evaluate_relay_state