########################################################################################## ########################################################################################## # POOL LIGHT POWER AND TIMER # Controlled by a Athom Smart Plug V1 # package_import_url: github://athom-tech/athom-configs/athom-smart-plug.yaml # # V2.0 2025-06-05 YAML Tidyups # # INSTRUCTIONS # - It allows a device to work in a standalone operation # - On startup, it will turn on for (startup_duration) hours then go into timer mode # - The timer has a morning and evening time (but no weekday/weekend setting) # - 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 4 on/off times via MQTT (See below commands) # - You can set 4 modes ON/OFF/TIMER/STARTUP via MQTT. That way, you can set to STARTUP for a short boost # - Any new timer times set via MQTT will be remembered though a reboot # - On a reboot, the device will always turn on for the Startup Duration (STARTUP mode, default 2 hours) # - TIMER mode will always be switched on after startup mode is complete # - 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/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/STARTUP : Turn on device for (startup_duration) hours then TIMER (also on startup) # # operation_mode: # 0 = OFF # 1 = ON # 2 = TIMER # 3 = STARTUP # ########################################################################################## ########################################################################################## ########################################################################################## # 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-poollightpower" friendly_name: "Pool Light Power" description_comment: "Pool Light Power, Athom Smart Plug Power Monitor" device_area: "Downstairs Kitchen" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming project_name: "Athom Technology.Smart Plug V1" # Project Details project_version: "v2.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-poollightpower_ip # Device Settings log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE update_interval: "60s" # update time for for general sensors etc # Timer Settings relay_icon: "mdi:light-flood-up" 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 startup_duration: "180" # Minutes to stay ON in STARTUP 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 ########################################################################################## # PACKAGES # https://esphome.io/components/esphome.html ########################################################################################## packages: common_wifi: !include file: common/network_common.yaml vars: local_static_ip_address: "${static_ip_address}" local_ota_pass: "${ota_pass}" common_api: !include file: common/api_common.yaml vars: local_api_key: "${api_key}" # common_webportal: !include # file: common/webportal_common.yaml common_mqtt: !include file: common/mqtt_common.yaml common_sntp: !include file: common/sntp_common.yaml common_general_sensors: !include file: common/sensors_common.yaml vars: local_friendly_name: "${friendly_name}" local_update_interval: "${update_interval}" ########################################################################################## # 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}" ########################################################################################## # 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 preferences: flash_write_interval: 5min mdns: disabled: false #dashboard_import: # package_import_url: github://athom-tech/esp32-configs/athom-smart-plug.yaml ########################################################################################## # ESPHome LOGGING # 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 # 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}" #################################################### # operation_mode: # 0 = OFF # 1 = ON # 2 = TIMER # 3 = STARTUP #################################################### - id: operation_mode type: int restore_value: false initial_value: "2" #################################################### # current_mins is set if SNTP is invalid. # We assume user powers on the device at 12:00 noon # => 12 * 60 = 720 minutes from midnight. # Not restored, so it resets each boot. #################################################### - id: current_mins type: int restore_value: false initial_value: "720" # 720 is 12:00 Noon #################################################### # startup_timer: counts minutes in STARTUP mode # After 'startup_duration' minutes, revert to TIMER. # Not restored, so each boot starts fresh at 0. #################################################### - id: startup_timer type: int restore_value: false initial_value: "0" #################################################### # total_energy: cumulative power # Restored, so keeps counting up. #################################################### - id: total_energy type: float restore_value: yes initial_value: "0.0" ########################################################################################## # UART Bus # https://esphome.io/components/uart.html ########################################################################################## #uart: # rx_pin: RX # baud_rate: 4800 # parity: EVEN ########################################################################################## # STATUS LED # https://esphome.io/components/status_led.html ########################################################################################## status_led: pin: number: GPIO13 inverted: True ########################################################################################## # Text Sensors # https://esphome.io/components/text_sensor/index.html ########################################################################################## text_sensor: ############################ # MQTT Subscriptions ############################ #################################################### # Subscribe to the Morning On time, format "HH:MM" # We check x.size() == 5 and x[2] == ':', # then parse x.substr(0,2) and x.substr(3,2) # std::string uses 'substr', not 'substring'. #################################################### - 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()); } #################################################### # Subscribe to operation mode: # OFF, ON, TIMER, STARTUP # We do case-insensitive compare using strcasecmp # (Requires typically included in ESPHome) #################################################### - platform: mqtt_subscribe name: "Operation Mode Setting" id: timer_operation_mode_topic topic: "${mqtt_timer_topic}/operation" # STARTUP,ON,OFF,TIMER internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value on_value: then: - lambda: |- /* * In standard C++ (ESPHome), no 'equalsIgnoreCase()'. * We use 'strcasecmp' for case-insensitive compare. * Returns 0 if they match ignoring case. */ 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(), "STARTUP") == 0) { id(operation_mode) = 3; id(startup_timer) = 0; // Reset the startup timer to zero ESP_LOGI("timer","Operation mode set to STARTUP"); } else { ESP_LOGW("timer","Invalid operation mode: %s", x.c_str()); } ###################################################### # Expose the current operation mode (OFF, ON, TIMER, STARTUP) ###################################################### - platform: template name: "Operation Mode State" lambda: |- // 0=OFF, 1=ON, 2=TIMER, 3=STARTUP switch (id(operation_mode)) { case 0: return {"OFF"}; case 1: return {"ON"}; case 2: return {"TIMER"}; case 3: return {"STARTUP"}; default: return {"UNKNOWN"}; } #update_interval: "${update_interval}" 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 buffer size to 8 just to be safe // 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 buffer size to 8 just to be safe // 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 buffer size to 8 just to be safe // 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: 3 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 0 (Timer) id(relay).turn_off(); id(operation_mode) = 2; } else { // Relay is OFF: turn it ON and set mode to 3 (Startup) id(relay).turn_on(); id(operation_mode) = 3; } - platform: template name: "Relay Status" lambda: |- return id(relay).state; ########################################################################################## # Sensors # https://esphome.io/components/text_sensor/index.html ########################################################################################## sensor: - platform: template name: "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 calculation for total energy used, ever (so long as wuth esp8266 restore_from_flash: true) - platform: template name: "Total Energy" id: total_energy_sensor unit_of_measurement: "kWh" device_class: "energy" state_class: "total_increasing" icon: mdi:lightning-bolt accuracy_decimals: "3" lambda: |- return id(total_energy); update_interval: "${update_interval}" # 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: 10s 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 (startup) 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 = ${startup_duration} - id(startup_timer); } // never return negative return rem > 0 ? rem : 0; ############################################# # CSE7766 POWER SENSOR # https://esphome.io/components/sensor/cse7766.html ############################################# - platform: hlw8012 id: athom_hlw8012 sel_pin: number: GPIO12 inverted: True cf_pin: GPIO4 cf1_pin: GPIO5 voltage_divider: 780 current: name: "Current" id: current unit_of_measurement: A accuracy_decimals: 2 icon: mdi:current-ac filters: - calibrate_linear: - 0.0000 -> 0.0110 # Relay off no load - 0.0097 -> 0.0260 # Relay on no load - 0.9270 -> 0.7570 - 2.0133 -> 1.6330 - 2.9307 -> 2.3750 - 5.4848 -> 4.4210 - 8.4308 -> 6.8330 - 9.9171 -> 7.9830 # Normalize for plug load - lambda: if (x < 0.0260) return 0; else return (x - 0.0260); on_value_range: - above: ${current_limit} then: - switch.turn_off: relay voltage: name: "Voltage" id: voltage unit_of_measurement: V accuracy_decimals: 1 icon: mdi:sine-wave filters: - skip_initial: 2 power: name: "Power" id: power_sensor unit_of_measurement: W accuracy_decimals: 1 icon: mdi:power filters: - calibrate_linear: - 0.0000 -> 0.5900 # Relay off no load - 0.0000 -> 1.5600 # Relay on no load - 198.5129 -> 87.8300 - 434.2469 -> 189.5000 - 628.6241 -> 273.9000 - 1067.0067 -> 460.1000 - 1619.8098 -> 699.2000 - 2043.0282 -> 885.0000 # Normalize for plug load - lambda: if (x < 1.5600) return 0; else return (x - 1.5600); change_mode_every: 1 update_interval: 5s # Shows the Energy kWh since the device was last started energy: name: "Energy (Since Restart)" id: energy icon: mdi:lightning-bolt unit_of_measurement: kWh accuracy_decimals: 3 filters: # Multiplication factor from W to kW is 0.001 - multiply: 0.001 on_value: then: - lambda: |- static float previous_energy_value = 0.0; float current_energy_value = id(energy).state; id(total_energy) += current_energy_value - previous_energy_value; previous_energy_value = current_energy_value; id(total_energy_sensor).update(); # internal: ${hide_energy_sensor} ############################################# # Total Daily Energy # https://esphome.io/components/sensor/total_daily_energy.html ############################################# - platform: total_daily_energy name: "Total Daily Energy" restore: true power_id: power_sensor unit_of_measurement: kWh icon: mdi:hours-24 accuracy_decimals: 3 filters: - multiply: 0.001 ################################################################################################# # 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}" ################################################################################################# # BUTTON COMPONENT # https://esphome.io/components/button/index.html ################################################################################################# button: - platform: template name: "Startup: ${startup_duration} Minutes" id: startup_button icon: "mdi:play-circle-outline" # optional, pick any MaterialDesign icon you like on_press: then: # 1) set the mode to STARTUP (3) - lambda: |- id(startup_timer) = 0; // Reset the startup timer to zero id(operation_mode) = 3; // Set to STARTUP ESP_LOGD("main", "operation_mode set to %d via STARTUP button", id(operation_mode)); # 2) turn on the relay switch - switch.turn_on: id: relay ################################################################################################# # SELECT COMPONENT # https://esphome.io/components/select/index.html ################################################################################################# select: - platform: template name: "Operation Mode" id: operation_mode_select update_interval: 5s # poll every 5 s for external changes options: - "OFF" - "ON" - "TIMER" - "STARTUP" # Getter: maps your integer into one of the four strings lambda: |- switch (id(operation_mode)) { case 1: return std::string("ON"); case 2: return std::string("TIMER"); case 3: return std::string("STARTUP"); default: return std::string("OFF"); } # set_action: called when you pick an option in HA 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(startup_timer) = 0; // Reset the startup timer to zero id(operation_mode) = 3; /* STARTUP */ } ESP_LOGD("main", "operation_mode set to %d", id(operation_mode)); ################################################################################################# # INTERVAL COMPONENT # https://esphome.io/components/interval.html ################################################################################################# interval: - interval: "1min" # Must be 1min as this is used to calculate times then: - lambda: |- // Do we have correct time from SNTP? If not... if (!id(time_sync).has_state()) { // Set minutes since midnight id(current_mins) = id(current_mins) +1 ; // wrap around at 1440 => next day if (id(current_mins) >= 1440) { id(current_mins) = 0; } // If we do have proper SNMP time... } else { // Use real time from SNTP auto now = id(sntp_time).now(); id(current_mins) = now.hour * 60 + now.minute; } // operation_mode: // 0 = OFF // 1 = ON // 2 = TIMER // 3 = STARTUP int mode = id(operation_mode); ////////////////////////////////////////////////// // STARTUP MODE: Relay ON for 'startup_duration' // minutes, then automatically revert to TIMER. ////////////////////////////////////////////////// if (mode == 3) { id(startup_timer) = id(startup_timer) + 1 ; // works as long as update_interval in seconds // Compare with the substitution startup_duration if (id(startup_timer) < (int) ${startup_duration}) { // Still within the STARTUP period => turn relay on id(relay).turn_on(); } else { // After 'startup_duration' minutes => switch to TIMER id(operation_mode) = 2; id(mqtt_client).publish("${mqtt_timer_topic}/operation", "TIMER"); } // Skip the rest of the logic ESP_LOGI("startup_timer", "startup_timer=%d", id(startup_timer)); return; } ////////////////////////////////////////////////// // OFF MODE => always off ////////////////////////////////////////////////// if (mode == 0) { id(relay).turn_off(); return; } ////////////////////////////////////////////////// // ON MODE => always on ////////////////////////////////////////////////// if (mode == 1) { id(relay).turn_on(); return; } ////////////////////////////////////////////////// // TIMER MODE => follow morning/evening schedule // using SNTP if valid, else current_mins ////////////////////////////////////////////////// if (mode == 2) { bool should_on = false; // Check morning window // Example: morning_on=360 => 06:00, morning_off=480 => 08:00 // If current_mins in [360..480), should_on = true if (id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off) ) { should_on = true; } // Check evening window // Example: evening_on=1260 => 21:00, evening_off=1440 => midnight if (id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off) ) { should_on = true; } // Final relay state based on schedule if (should_on) { id(relay).turn_on(); } else { id(relay).turn_off(); } }