#:########################################################################################:# # TITLE: POOL PUMP POWER AND TIMER # zorruno.com layout v1.1 2026 #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-poolpumppower.yaml #:########################################################################################:# # VERSIONS: # V2.5 2026-02-25 Updated yaml to zorruno layout V1.1 # V2.4 2026-01-31 Added Morning/Evening Timer Enable switches (gate timer windows) # V2.3 2025-06-19 Minor changes to remember mode in reboot and to ensure offline functionality # 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 and startup to boost # V2.0 2025-06-05 YAML Tidyups #:########################################################################################:# # HARDWARE: # - Athom Smart Plug V3 (ESP32-C3) controlling pool pump relay + power monitoring # - dashboard_import: github://athom-tech/esp32-configs/athom-smart-plug.yaml #:########################################################################################:# # OPERATION NOTES: # - Standalone timer style operation (morning and evening windows, no weekday/weekend). # - Morning Timer Enable / Evening Timer Enable switches gate their timer windows (default ON). # - Default values are set, but changed values are remembered in flash. # - Uses SNTP for accurate time when WiFi/networking is available. # - If on a network and MQTT is available, you can set on/off times and operation mode via MQTT. # - Operation modes: OFF / ON / TIMER / BOOST. BOOST is a oneshot that reverts back to TIMER. # - On startup or reboot, the device enters BOOST for the configured BOOST duration, then returns to TIMER. #:########################################################################################:# # MQTT COMMANDS: # Values are applied on the update_interval, not instantly. # Use 00:00 in 24hr format for time settings. # ${mqtt_timer_topic}/morning-on payload "HH:MM" # ${mqtt_timer_topic}/morning-off payload "HH:MM" # ${mqtt_timer_topic}/evening-on payload "HH:MM" # ${mqtt_timer_topic}/evening-off payload "HH:MM" # ${mqtt_timer_topic}/boost-time payload "0".."1439" (minutes) # ${mqtt_timer_topic}/operation payload "ON","OFF","TIMER","BOOST" # # operation_mode: # 0 = OFF # 1 = ON # 2 = TIMER # 3 = BOOST #:########################################################################################:# # OFFLINE NOTES: # a) Home Assistant OFFLINE, but Network and MQTT ONLINE # - Timer logic continues to run on the device. # - MQTT commands for schedule/mode updates still work. # b) MQTT OFFLINE (or HA/MQTT OFFLINE) # - If HA API is available, device can still be controlled from HA. # - Without MQTT, schedule/mode changes via MQTT will not work. # c) Entire WiFi/Network OFFLINE # - No HA API and no MQTT control available. # - Timer mode continues using the fallback clock. # - Loss of network will drift the timeclocks; time of day can be reset by powering off then on again at 12pm (midday). #:########################################################################################:# #:########################################################################################:# # SUBSTITUTIONS: 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-poolpumppower" friendly_name: "Pool Pump Power" description_comment: "Pool Pump Power :: Athom Smart Plug Power V3 (Layout V1.1)" device_area: "Outside" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming project_name: "Athom Technology.Smart Plug V3" project_version: "v2.5" # Updated yaml to zorruno layout V1.1 # Passwords api_key: !secret esp-poolpumppower_api_key # unfortunately you can't use substitutions inside secrets names ota_pass: !secret esp-poolpumppower_ota_pass # unfortunately you can't use substitutions inside secrets names static_ip_address: !secret esp-poolpumppower_ip # If we are changing IP addresses, you must update the current IP address here, otherwise it remains # Don't forget to switch it back when changed. current_ip_address: ${static_ip_address} # Device Settings log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE update_interval: "20s" # update time for general sensors etc # Timer Settings relay_icon: "mdi:pool" current_limit: "10" # Current limit in amps. AU plug = 10. IL, BR, EU, UK, US plug = 16. mqtt_timer_topic: "viewroad-commands/poolpump-timer" boost_duration_default: "60" # minutes to stay ON in BOOST mode before reverting to TIMER morning_on_default: "450" # 07:30 => 450 morning_off_default: "450" # 07:30 => 450 (same as ON, no morning schedule) evening_on_default: "1260" # 21:00 => 1260 evening_off_default: "1350" # 22:30 => 1350 #:########################################################################################:# # PACKAGES: Included Common Packages # https://esphome.io/components/packages.html #:########################################################################################:# packages: #### 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}" local_current_ip_address: "${current_ip_address}" #### 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}" #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" #### WEB PORTAL #### #common_webportal: !include common/webportal_common.yaml #### SNTP (Only use if you want/need accurate timeclocks) #### common_sntp: !include common/sntp_common.yaml #### DIAGNOSTICS Sensors #### diag_basic: !include common/include_basic_diag_sensors.yaml diag_more: !include common/include_more_diag_sensors.yaml diag_debug: !include common/include_debug_diag_sensors.yaml diag_resetcount: !include common/include_resetcount_diag_sensors.yaml #### Device Specific included packages #### common_athompowermonV3: !include file: common/athompowermonv3_common.yaml vars: local_current_limit: "${current_limit}" #:########################################################################################:# # 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: board_build.mcu: esp32c3 board_build.variant: esp32c3 board_build.flash_mode: dio on_boot: then: - script.execute: evaluate_relay_state #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp32/ #:########################################################################################:# esp32: board: esp32-c3-devkitm-1 flash_size: 4MB variant: ESP32C3 framework: type: esp-idf version: recommended preferences: flash_write_interval: 5min esp32_improv: authorizer: none #:########################################################################################:# # LOGGING: ESPHome Logging Enable # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 0 #:########################################################################################:# # GLOBAL VARIABLES: # https://esphome.io/guides/automations.html#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" #:########################################################################################:# # TEXT SENSOR: # https://esphome.io/components/text_sensor/index.html #:########################################################################################:# text_sensor: ############################ # MQTT Subscriptions ############################ - platform: mqtt_subscribe name: "Morning On Time Setting" id: morning_on_topic topic: "${mqtt_timer_topic}/morning-on" internal: true 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_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()); } - platform: mqtt_subscribe name: "Morning Off Time Setting" id: morning_off_topic topic: "${mqtt_timer_topic}/morning-off" internal: true 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()); } - platform: mqtt_subscribe name: "Evening On Time Setting" id: evening_on_topic topic: "${mqtt_timer_topic}/evening-on" internal: true 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()); } - platform: mqtt_subscribe name: "Evening Off Time Setting" id: evening_off_topic topic: "${mqtt_timer_topic}/evening-off" internal: true 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()); } - platform: mqtt_subscribe name: "Boost Duration" id: boost_time_topic topic: "${mqtt_timer_topic}/boost-time" internal: true on_value: then: - lambda: |- char *endptr; long v = strtol(x.c_str(), &endptr, 10); 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) = (int) v; } - platform: mqtt_subscribe id: timer_operation_mode_topic topic: "${mqtt_timer_topic}/operation" internal: true 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 - platform: template name: "Operation Mode State" lambda: |- 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 - platform: template name: "Timeclock: Morning On Time" lambda: |- int hour = id(morning_on) / 60; int minute = id(morning_on) % 60; char buff[16]; snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); return { std::string(buff) }; update_interval: "${update_interval}" - platform: template name: "Timeclock: Morning Off Time" lambda: |- int hour = id(morning_off) / 60; int minute = id(morning_off) % 60; char buff[16]; snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); return { std::string(buff) }; update_interval: "${update_interval}" - platform: template name: "Timeclock: Evening On Time" lambda: |- int hour = id(evening_on) / 60; int minute = id(evening_on) % 60; char buff[16]; snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); return { std::string(buff) }; update_interval: "${update_interval}" - platform: template name: "Timeclock: Evening Off Time" lambda: |- int hour = id(evening_off) / 60; int minute = id(evening_off) % 60; 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: GPIO3 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) { id(relay).turn_off(); id(operation_mode) = 2; } else { id(relay).turn_on(); id(operation_mode) = 3; } - platform: template name: "Relay Status" lambda: |- return id(relay).state; #:########################################################################################:# # SENSORS: # https://esphome.io/components/sensor/ #:########################################################################################:# 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 lambda: |- return id(current_mins); - platform: template name: "Timer Minutes Remaining" id: timer_minutes_remaining unit_of_measurement: "mins" update_interval: 5s accuracy_decimals: 0 lambda: |- if (!id(relay).state) return 0; int rem = 0; if (id(operation_mode) == 2) { if (id(morning_timer_enable).state && id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off)) { rem = id(morning_off) - id(current_mins); } else if (id(evening_timer_enable).state && id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off)) { rem = id(evening_off) - id(current_mins); } else { rem = 0; } } else if (id(operation_mode) == 3) { rem = id(boost_duration) - id(boost_timer); } return rem > 0 ? rem : 0; #:########################################################################################:# # SWITCH COMPONENT: # https://esphome.io/components/switch/ #:########################################################################################:# switch: - platform: gpio name: "Power Output" pin: GPIO5 id: relay restore_mode: RESTORE_DEFAULT_OFF icon: "${relay_icon}" # Timer Enable Switches (gate schedule windows) - platform: template name: "Morning Timer Enable" id: morning_timer_enable icon: "mdi:timer-outline" optimistic: true restore_mode: RESTORE_DEFAULT_ON entity_category: config on_turn_on: - script.execute: evaluate_relay_state on_turn_off: - script.execute: evaluate_relay_state - platform: template name: "Evening Timer Enable" id: evening_timer_enable icon: "mdi:timer-outline" optimistic: true restore_mode: RESTORE_DEFAULT_ON entity_category: config on_turn_on: - script.execute: evaluate_relay_state on_turn_off: - script.execute: evaluate_relay_state #:########################################################################################:# # 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: - lambda: |- id(boost_timer) = 0; id(operation_mode) = 3; - script.execute: evaluate_relay_state - platform: template name: "Default timer settings" id: default_timer_settings_button icon: "mdi:restore" on_press: - lambda: |- 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}; id(operation_mode) = 2; id(boost_timer) = 0; ESP_LOGI("timer","Default timer settings applied"); - switch.turn_on: morning_timer_enable - switch.turn_on: evening_timer_enable - 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" 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"); } 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 { id(boost_timer) = 0; id(operation_mode) = 3; } - script.execute: evaluate_relay_state #:########################################################################################:# # SCRIPT COMPONENT: # https://esphome.io/components/script.html #:########################################################################################:# script: - id: evaluate_relay_state then: - lambda: |- int mode = id(operation_mode); if (mode == 3) { id(relay).turn_on(); return; } if (mode == 0) { id(relay).turn_off(); return; } if (mode == 1) { id(relay).turn_on(); return; } // TIMER -> follow schedule windows (gated by enable switches) bool should_on = false; if (id(morning_timer_enable).state) { if (id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off)) { should_on = true; } } if (id(evening_timer_enable).state) { if (id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off)) { should_on = true; } } if (should_on) id(relay).turn_on(); else id(relay).turn_off(); #:########################################################################################:# # INTERVAL COMPONENT: # https://esphome.io/components/interval.html #:########################################################################################:# 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; } // BOOST timer handling if (id(operation_mode) == 3) { id(boost_timer)++; if (id(boost_timer) >= id(boost_duration)) { id(operation_mode) = 2; } } - script.execute: evaluate_relay_state