From a060d819cda9250525e28cf361326f6240fda512 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Sep 2025 20:53:14 +1200 Subject: [PATCH] pool light esp, gas heater esp, HA Overview display --- .../{ => archive}/bt-fridge-temps.yaml.temp | 0 .../archive/esp-datacupboard-right.yaml.old | 130 +++ .../esp-datacupboardpower-left.yaml.old | 126 +++ .../esp-datacupboardpowermon-right.yaml.old | 131 +++ .../esp-downstdishwpower.yaml.old} | 0 .../archive/esp-poollightpower.yaml.backup | 705 +++++++++++++ .../athom-without-relay-plug-f26f8b.yaml.old | 18 + .../.esphome/external_components/2f2a5be3 | 1 + esphome/common/athompowermonv2_common.yaml | 180 ++++ .../common/athompowermonv2norelay_common.yaml | 187 ++++ esphome/common/network_common.yaml | 6 +- esphome/esp-datacupboardpower-left.yaml | 128 +++ esphome/esp-datacupboardpower-right.yaml | 128 +++ esphome/esp-downstairsdishwasher.yaml | 186 ++++ esphome/esp-gasheater.yaml | 936 ++++++++++++++++++ esphome/esp-poollightpower.yaml | 375 ++++++- packages/ha_summary.yaml | 236 +++++ 17 files changed, 3436 insertions(+), 37 deletions(-) rename esphome/{ => archive}/bt-fridge-temps.yaml.temp (100%) create mode 100644 esphome/archive/esp-datacupboard-right.yaml.old create mode 100644 esphome/archive/esp-datacupboardpower-left.yaml.old create mode 100644 esphome/archive/esp-datacupboardpowermon-right.yaml.old rename esphome/{esp-downstdishwpower.yaml => archive/esp-downstdishwpower.yaml.old} (100%) create mode 100644 esphome/archive/esp-poollightpower.yaml.backup create mode 100644 esphome/athom-without-relay-plug-f26f8b.yaml.old create mode 160000 esphome/common/.esphome/external_components/2f2a5be3 create mode 100644 esphome/common/athompowermonv2_common.yaml create mode 100644 esphome/common/athompowermonv2norelay_common.yaml create mode 100644 esphome/esp-datacupboardpower-left.yaml create mode 100644 esphome/esp-datacupboardpower-right.yaml create mode 100644 esphome/esp-downstairsdishwasher.yaml create mode 100644 esphome/esp-gasheater.yaml create mode 100644 packages/ha_summary.yaml diff --git a/esphome/bt-fridge-temps.yaml.temp b/esphome/archive/bt-fridge-temps.yaml.temp similarity index 100% rename from esphome/bt-fridge-temps.yaml.temp rename to esphome/archive/bt-fridge-temps.yaml.temp diff --git a/esphome/archive/esp-datacupboard-right.yaml.old b/esphome/archive/esp-datacupboard-right.yaml.old new file mode 100644 index 0000000..6f80cd5 --- /dev/null +++ b/esphome/archive/esp-datacupboard-right.yaml.old @@ -0,0 +1,130 @@ +# key: RhIxVv/Zu8IbuULq1upidP8Z1HxFDmVNx8kUvfPSRNs= +########################################################################################## +########################################################################################## +# DATA CUPBOARD POWER MONITOR - RIGHT +# +# Controlled by a Athom Smart Plug V2 No Relay +# https://github.com/athom-tech/athom-configs/blob/main/athom-without-relay-plug.yaml +# +# V1.0 2025-08-28 First Setup +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-datacupboard-right" + friendly_name: "Data Cupboard Power Monitor - Right" + description_comment: "Data Cupboard Right :: Athom Smart Plug Power V2 (No relay)" + device_area: "Data Cupboard" + + # Project Naming + project_name: "Athom Technology.Smart Plug V2 NR" + project_version: "v1.0" + + # Passwords (unfortunately you cannot use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-datacupboard-right_ip + #static_ip_address: "172.31.10.95" + + # Device General Settings + log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for general sensors etc + + # Device Specific Settings (Athom Power Monitor Plug V2) + sensor_update_interval: "10s" + hide_energy_sensor: "true" # boolean (used by "internal:") + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + + # Network and Wi-Fi + 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}" + + # Native API + common_api: !include + file: common/api_common.yaml + vars: + local_api_key: "${api_key}" + #local_api_key: RhIxVv/Zu8IbuULq1upidP8Z1HxFDmVNx8kUvfPSRNs= + +# # 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}" + + # Time/SNTP + 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}" + + # Device Specific included packages + # Athom Power Monitor Plug V2 (No Relay) + common_athompowermonV2: !include + file: common/athompowermonv2norelay_common.yaml + vars: + local_friendly_name: "${friendly_name}" + sensor_update_interval: "${sensor_update_interval}" + hide_energy_sensor: "${hide_energy_sensor}" + +########################################################################################## +# ESPHome +# https://esphome.io/components/esphome.html +########################################################################################## +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + 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/esp8266.html +########################################################################################## +esp8266: + board: esp8285 + restore_from_flash: true + early_pin_init: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false + +########################################################################################## +# ESPHome LOGGING +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 # free UART for CSE7766 + diff --git a/esphome/archive/esp-datacupboardpower-left.yaml.old b/esphome/archive/esp-datacupboardpower-left.yaml.old new file mode 100644 index 0000000..08be401 --- /dev/null +++ b/esphome/archive/esp-datacupboardpower-left.yaml.old @@ -0,0 +1,126 @@ +########################################################################################## +########################################################################################## +# DATA CUPBOARD POWER MONITOR - LEFT +# +# Controlled by a Athom Smart Plug V2 No Relay +# https://github.com/athom-tech/athom-configs/blob/main/athom-without-relay-plug.yaml +# +# V1.0 2025-08-28 First Setup +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-datacupboard-left" + friendly_name: "Data Cupboard Power Monitor - Left" + description_comment: "Data Cupboard Left :: Athom Smart Plug Power V2 No relay" + device_area: "Data Cupboard" + + # Project Naming + project_name: "Athom Technology.Smart Plug V2 NR" + project_version: "v1.0" + + # Passwords (unfortunately you cannot use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-datacupboard-left_ip + + # Device General Settings + log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for general sensors etc + + # Device Specific Settings (Athom Power Monitor Plug V2) + sensor_update_interval: "10s" + hide_energy_sensor: "true" # boolean (used by "internal:") + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + + # Network and Wi-Fi + 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}" + + # Native API + common_api: !include + file: common/api_common.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}" + + # Time/SNTP + 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}" + + # Device Specific included packages + # Athom Power Monitor Plug V2 (No Relay) + common_athompowermonV2: !include + file: common/athompowermonv2norelay_common.yaml + vars: + local_friendly_name: "${friendly_name}" + sensor_update_interval: "${sensor_update_interval}" + hide_energy_sensor: "${hide_energy_sensor}" + +########################################################################################## +# ESPHome +# https://esphome.io/components/esphome.html +########################################################################################## +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + 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/esp8266.html +########################################################################################## +esp8266: + board: esp8285 + restore_from_flash: true + early_pin_init: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false + +########################################################################################## +# ESPHome LOGGING +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 # free UART for CSE7766 diff --git a/esphome/archive/esp-datacupboardpowermon-right.yaml.old b/esphome/archive/esp-datacupboardpowermon-right.yaml.old new file mode 100644 index 0000000..8bd14f5 --- /dev/null +++ b/esphome/archive/esp-datacupboardpowermon-right.yaml.old @@ -0,0 +1,131 @@ +# key: EW43Gye1M+RLJkFHn1fN97pWImdurb78eD/FfIQh9nU= +# key: RhIxVv/Zu8IbuULq1upidP8Z1HxFDmVNx8kUvfPSRNs= +########################################################################################## +########################################################################################## +# DATA CUPBOARD POWER MONITOR - RIGHT +# +# Controlled by a Athom Smart Plug V2 No Relay +# https://github.com/athom-tech/athom-configs/blob/main/athom-without-relay-plug.yaml +# +# V1.0 2025-08-28 First Setup +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-datacupboard-right" + friendly_name: "Data Cupboard Power Monitor - Right" + description_comment: "Data Cupboard Right :: Athom Smart Plug Power V2 (No relay)" + device_area: "Data Cupboard" + + # Project Naming + project_name: "Athom Technology.Smart Plug V2 NR" + project_version: "v1.0" + + # Passwords (unfortunately you cannot use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-datacupboard-right_ip + #static_ip_address: "172.31.10.95" + + # Device General Settings + log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for general sensors etc + + # Device Specific Settings (Athom Power Monitor Plug V2) + sensor_update_interval: "10s" + hide_energy_sensor: "true" # boolean (used by "internal:") + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + + # Network and Wi-Fi + 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}" + + # Native API + common_api: !include + file: common/api_common.yaml + vars: + #local_api_key: "${api_key}" + local_api_key: "EW43Gye1M+RLJkFHn1fN97pWImdurb78eD/FfIQh9nU=" + +# # 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}" + + # Time/SNTP + 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}" + + # Device Specific included packages + # Athom Power Monitor Plug V2 (No Relay) + common_athompowermonV2: !include + file: common/athompowermonv2norelay_common.yaml + vars: + local_friendly_name: "${friendly_name}" + sensor_update_interval: "${sensor_update_interval}" + hide_energy_sensor: "${hide_energy_sensor}" + +########################################################################################## +# ESPHome +# https://esphome.io/components/esphome.html +########################################################################################## +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + 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/esp8266.html +########################################################################################## +esp8266: + board: esp8285 + restore_from_flash: true + early_pin_init: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false + +########################################################################################## +# ESPHome LOGGING +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 # free UART for CSE7766 + diff --git a/esphome/esp-downstdishwpower.yaml b/esphome/archive/esp-downstdishwpower.yaml.old similarity index 100% rename from esphome/esp-downstdishwpower.yaml rename to esphome/archive/esp-downstdishwpower.yaml.old diff --git a/esphome/archive/esp-poollightpower.yaml.backup b/esphome/archive/esp-poollightpower.yaml.backup new file mode 100644 index 0000000..b88c45a --- /dev/null +++ b/esphome/archive/esp-poollightpower.yaml.backup @@ -0,0 +1,705 @@ +########################################################################################## +########################################################################################## +# POOL LIGHT POWER AND TIMER +# Controlled by a Athom Smart Plug V1 +# +# dashboard_import: +# package_import_url: github://athom-tech/esp32-configs/athom-smart-plug.yaml +# +# 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-poollightpower" + friendly_name: "Pool Light Power" + description_comment: "Pool Light Power :: Athom Smart Plug Power V1" + device_area: "Outside" # 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.4" # 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: "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 + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + 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}" + 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 + vars: + local_device_name: "${device_name}" + 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}" + + # Device Specific included packages + common_athompowermonV1: !include + file: common/athompowermonv1_common.yaml + vars: + local_friendly_name: "${friendly_name}" + 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: + # 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" + + #################################################### + # 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. + #################################################### + - id: current_mins + type: int + restore_value: true + initial_value: "720" # 720 is 12:00 Noon + + #################################################### + # 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 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()); + } + #################################################### + # 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 0–1439 + 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(v); + } + + #################################################### + # Subscribe to operation mode: + # OFF, ON, TIMER, BOOST + # We do case-insensitive compare using strcasecmp + # (Requires typically included in ESPHome) + #################################################### + # 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 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: 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 0 (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; + } + + - platform: template + name: "Relay Status" + lambda: |- + return id(relay).state; + +########################################################################################## +# 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}" + +################################################################################################# +# 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 + +################################################################################################# +# SCRIPT COMPONENT +# https://esphome.io/components/script.html +################################################################################################# +# Script: evaluate and drive the relay +script: + - id: evaluate_relay_state + then: + - lambda: |- + int mode = id(operation_mode); + + // BOOST just forces the relay on + if (mode == 3) { + id(relay).turn_on(); + return; + } + + // OFF → always off + if (mode == 0) { + id(relay).turn_off(); + return; + } + + // ON → always on + if (mode == 1) { + id(relay).turn_on(); + return; + } + + // TIMER → follow schedule windows + { + 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; + if (should_on) id(relay).turn_on(); + else id(relay).turn_off(); + } + +################################################################################################# +# 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 + diff --git a/esphome/athom-without-relay-plug-f26f8b.yaml.old b/esphome/athom-without-relay-plug-f26f8b.yaml.old new file mode 100644 index 0000000..8c41634 --- /dev/null +++ b/esphome/athom-without-relay-plug-f26f8b.yaml.old @@ -0,0 +1,18 @@ +substitutions: + name: athom-without-relay-plug-f26f8b + friendly_name: esp-datacupboard-right +packages: + Athom Technology.Athom Without Relay Plug: github://athom-tech/athom-configs/athom-without-relay-plug.yaml +esphome: + name: ${name} + name_add_mac_suffix: false + friendly_name: ${friendly_name} +api: + encryption: + #key: be5SnhMQOzoDFe9rhiIs7bYA+TOwN/dOSUWxFLhhhaI= + key: RhIxVv/Zu8IbuULq1upidP8Z1HxFDmVNx8kUvfPSRNs= + + +wifi: + ssid: !secret ha_wifi_ssid + password: !secret ha_wifi_password diff --git a/esphome/common/.esphome/external_components/2f2a5be3 b/esphome/common/.esphome/external_components/2f2a5be3 new file mode 160000 index 0000000..b2cd0ac --- /dev/null +++ b/esphome/common/.esphome/external_components/2f2a5be3 @@ -0,0 +1 @@ +Subproject commit b2cd0ac45a6941fdce5a9eac4ebf4afd04444f4b diff --git a/esphome/common/athompowermonv2_common.yaml b/esphome/common/athompowermonv2_common.yaml new file mode 100644 index 0000000..9728ff5 --- /dev/null +++ b/esphome/common/athompowermonv2_common.yaml @@ -0,0 +1,180 @@ +########################################################################################## +# GLOBAL VARIABLES +# https://esphome.io/guides/automations.html?highlight=globals#global-variables +########################################################################################## +globals: + #################################################### + # total_energy: cumulative power + # Not restored, so each boot starts fresh at 0. + #################################################### + - id: total_energy + type: float + restore_value: yes + initial_value: '0.0' + +########################################################################################## +# UART +# https://esphome.io/components/uart/ +########################################################################################## +uart: + # Needed for cse776. Set logging baud to 0 also. + id: cse_uart + rx_pin: RX # GPIO3 on ESP8266 + baud_rate: 4800 + parity: EVEN # CSE7766 requires EVEN + +########################################################################################## +# GENERAL COMMON SENSORS +# https://esphome.io/components/sensor/ +########################################################################################## +sensor: + ############################################# + # CSE7766 POWER SENSOR + # https://esphome.io/components/sensor/cse7766.html + ############################################# + - platform: cse7766 + id: athom_cse7766 + uart_id: cse_uart + current: + name: "Current" + id: plug_current # Added ID so we can reference this in other sensors + icon: mdi:current-ac + filters: + - throttle_average: ${sensor_update_interval} + - lambda: if (x < 0.060) return 0.0; else return x; # For the chip will report less than 3w power when no load is connected + on_value_range: + - above: ${current_limit} + then: + - switch.turn_off: relay + + voltage: + name: "Voltage" + id: plug_voltage # Added ID so we can reference this in other sensors + icon: mdi:sine-wave + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + power: + name: "Power" + id: power_sensor + icon: mdi:power + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + - lambda: if (x < 3.0) return 0.0; else return x; # For the chip will report less than 3w power when no load is connected + + energy: + name: "Energy" + id: energy + icon: mdi:lightning-bolt + unit_of_measurement: kWh + internal: ${hide_energy_sensor} + filters: + - throttle: ${sensor_update_interval} + # Multiplication factor from W to kW is 0.001 + - multiply: 0.001 + accuracy_decimals: 2 + 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(); + + apparent_power: + name: "Apparent Power" + icon: mdi:power + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + reactive_power: + name: "Reactive Power" + icon: mdi:flash + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + power_factor: + name: "Power Factor" + icon: mdi:percent-outline + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + ############################################# + # Total Energy (All Time) + ############################################# + - 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: "60s" + + ############################################# + # 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 + +########################################################################################## +# STATUS LED +# https://esphome.io/components/status_led.html +########################################################################################## +status_led: + pin: + number: GPIO13 + inverted: True + +########################################################################################## +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +########################################################################################## +binary_sensor: + - platform: gpio + pin: + number: 5 + mode: INPUT_PULLUP + inverted: true + id: power_button + name: "Power Button" + disabled_by_default: true + on_multi_click: + - timing: + - ON for at most 1s + - OFF for at least 0.2s + then: + - switch.toggle: relay + #- timing: + # - ON for at least 4s + # then: + # - button.press: Reset + +########################################################################################## +# SWITCH COMPONENT +# https://esphome.io/components/switch/ +########################################################################################## +switch: + - platform: gpio + name: "Switch" + pin: GPIO12 + id: relay + restore_mode: ${relay_restore_mode} + icon: ${power_plug_type} diff --git a/esphome/common/athompowermonv2norelay_common.yaml b/esphome/common/athompowermonv2norelay_common.yaml new file mode 100644 index 0000000..ba31cbd --- /dev/null +++ b/esphome/common/athompowermonv2norelay_common.yaml @@ -0,0 +1,187 @@ +########################################################################################## +# GLOBAL VARIABLES +# https://esphome.io/guides/automations.html#global-variables +########################################################################################## +globals: + # total_energy: cumulative power (restored across reboots) + - id: total_energy + type: float + restore_value: yes + initial_value: '0.0' + +########################################################################################## +# UART +# https://esphome.io/components/uart/ +########################################################################################## +uart: + # Needed for cse776. Set logging baud to 0 also. + id: cse_uart + rx_pin: RX # GPIO3 on ESP8266 + baud_rate: 4800 + parity: EVEN # CSE7766 requires EVEN + +########################################################################################## +# SENSORS +# https://esphome.io/components/sensor/ +########################################################################################## +sensor: + ############################################# + # CSE7766 POWER SENSOR + # https://esphome.io/components/sensor/cse7766.html + ############################################# + - platform: cse7766 + id: athom_cse7766 + uart_id: cse_uart + + current: + name: "Current" + id: plug_current + icon: mdi:current-ac + filters: + - throttle_average: ${sensor_update_interval} + - lambda: if (x < 0.060) return 0.0; else return x; + + voltage: + name: "Voltage" + id: plug_voltage + icon: mdi:sine-wave + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + power: + name: "Power" + id: power_sensor + icon: mdi:power + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + - lambda: if (x < 3.0) return 0.0; else return x; + + energy: + name: "Energy" + id: energy + icon: mdi:lightning-bolt + unit_of_measurement: kWh + internal: ${hide_energy_sensor} + accuracy_decimals: 2 + filters: + - throttle: ${sensor_update_interval} + - multiply: 0.001 # Wh -> kWh + 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(); + + apparent_power: + name: "Apparent Power" + icon: mdi:power + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + reactive_power: + name: "Reactive Power" + icon: mdi:flash + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + power_factor: + name: "Power Factor" + icon: mdi:percent-outline + accuracy_decimals: 2 + filters: + - throttle_average: ${sensor_update_interval} + + ############################################# + # Total Energy (All Time) + ############################################# + - 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: "60s" + + ############################################# + # 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 + + ############################################# + # Energy Statistics (Dentra) + ############################################# + - platform: energy_statistics + total: total_energy_sensor + + energy_today: + name: "Energy Today" + id: total_energy_today + accuracy_decimals: 3 + icon: mdi:hours-24 + + energy_yesterday: + name: "Total Energy Yesterday" + id: total_energy_yesterday + accuracy_decimals: 3 + + energy_week: + name: "Total Energy Week" + id: total_energy_week + accuracy_decimals: 3 + + energy_month: + name: "Total Energy Month" + id: total_energy_month + accuracy_decimals: 3 + +########################################################################################## +# Dentra Components - Adds 'Platform - Energy Statistics' +# https://github.com/dentra/esphome-components/tree/master/components/energy_statistics +########################################################################################## +external_components: + - source: github://dentra/esphome-components + +########################################################################################## +# STATUS LED +# https://esphome.io/components/status_led.html +########################################################################################## +status_led: + pin: + number: GPIO13 + inverted: true + +########################################################################################## +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +########################################################################################## +binary_sensor: + - platform: gpio + pin: + number: 5 + mode: INPUT_PULLUP + inverted: true + id: power_button + name: "Power Button" + disabled_by_default: true + +# No relay hardware on this device; leaving the switch section out intentionally. diff --git a/esphome/common/network_common.yaml b/esphome/common/network_common.yaml index 92a359c..0dcd023 100644 --- a/esphome/common/network_common.yaml +++ b/esphome/common/network_common.yaml @@ -53,11 +53,11 @@ wifi: dns2: ${static_ip_dns2} use_address: ${local_static_ip_address} ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: ${local_device_name} AP + ssid: ${local_device_name} password: ${fallback_ap_password} ap_timeout: 10min # Time until it brings up fallback AP. default is 1min # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID - fast_connect: "${wifi_fast_connect}" + fast_connect: ${wifi_fast_connect} # Define dns domain / suffix to add to hostname domain: "${dns_domain}" @@ -71,7 +71,7 @@ ota: - platform: esphome password: ${local_ota_pass} version: 2 - #- platform: web_server + #- platform: web_server # Uncomment if you want to be able to do OTA with the web interface ############################################# # Safe Mode diff --git a/esphome/esp-datacupboardpower-left.yaml b/esphome/esp-datacupboardpower-left.yaml new file mode 100644 index 0000000..0fe551b --- /dev/null +++ b/esphome/esp-datacupboardpower-left.yaml @@ -0,0 +1,128 @@ +########################################################################################## +########################################################################################## +# DATA CUPBOARD POWER MONITOR - LEFT +# +# Controlled by a Athom Smart Plug V2 No Relay +# https://github.com/athom-tech/athom-configs/blob/main/athom-without-relay-plug.yaml +# +# V1.0 2025-08-28 First Setup +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-datacupboardpower-left" + friendly_name: "Data Cupboard Power Monitor - Left" + description_comment: "Data Cupboard Left :: Athom Smart Plug Power V2 (No relay)" + device_area: "Data Cupboard" + + # Project Naming + project_name: "Athom Technology.Smart Plug V2 NR" + project_version: "v1.0" + + # Passwords (unfortunately you cannot use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-datacupboardpower-left_ip + + # Device General Settings + log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for general sensors etc + + # Device Specific Settings (Athom Power Monitor Plug V2) + sensor_update_interval: "10s" + hide_energy_sensor: "true" # boolean (used by "internal:") + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + + # Network and Wi-Fi + 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}" + + # Native API + common_api: !include + file: common/api_common.yaml + vars: + #local_api_key: "${api_key}" + local_api_key: "5dW+2PdmYb7ovzvt/tGPbLU6XfBHsQeeUgZ5/ImQFA8=" + +# # 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}" + + # Time/SNTP + 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}" + + # Device Specific included packages + # Athom Power Monitor Plug V2 (No Relay) + common_athompowermonV2: !include + file: common/athompowermonv2norelay_common.yaml + vars: + local_friendly_name: "${friendly_name}" + sensor_update_interval: "${sensor_update_interval}" + hide_energy_sensor: "${hide_energy_sensor}" + +########################################################################################## +# ESPHome +# https://esphome.io/components/esphome.html +########################################################################################## +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + 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/esp8266.html +########################################################################################## +esp8266: + board: esp8285 + restore_from_flash: true + early_pin_init: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false + +########################################################################################## +# ESPHome LOGGING +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 # free UART for CSE7766 + diff --git a/esphome/esp-datacupboardpower-right.yaml b/esphome/esp-datacupboardpower-right.yaml new file mode 100644 index 0000000..c6f6fba --- /dev/null +++ b/esphome/esp-datacupboardpower-right.yaml @@ -0,0 +1,128 @@ + +########################################################################################## +########################################################################################## +# DATA CUPBOARD POWER MONITOR - RIGHT +# +# Controlled by a Athom Smart Plug V2 No Relay +# https://github.com/athom-tech/athom-configs/blob/main/athom-without-relay-plug.yaml +# +# V1.0 2025-08-28 First Setup +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-datacupboardpower-right" + friendly_name: "Data Cupboard Power Monitor - Right" + description_comment: "Data Cupboard Right :: Athom Smart Plug Power V2 (No relay)" + device_area: "Data Cupboard" + + # Project Naming + project_name: "Athom Technology.Smart Plug V2 NR" + project_version: "v1.0" + + # Passwords (unfortunately you cannot use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-datacupboardpower-right_ip + + # Device General Settings + log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for general sensors etc + + # Device Specific Settings (Athom Power Monitor Plug V2) + sensor_update_interval: "10s" + hide_energy_sensor: "true" # boolean (used by "internal:") + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + + # Network and Wi-Fi + 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}" + + # Native API + common_api: !include + file: common/api_common.yaml + vars: + #local_api_key: "${api_key}" + local_api_key: "tZ/djAp4qiM0wYCgUa1amg+wK7KTF9xR8bUFCaeVskM=" + + # 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}" + + # Time/SNTP + 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}" + + # Device Specific included packages + # Athom Power Monitor Plug V2 (No Relay) + common_athompowermonV2: !include + file: common/athompowermonv2norelay_common.yaml + vars: + local_friendly_name: "${friendly_name}" + sensor_update_interval: "${sensor_update_interval}" + hide_energy_sensor: "${hide_energy_sensor}" + +########################################################################################## +# ESPHome +# https://esphome.io/components/esphome.html +########################################################################################## +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + 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/esp8266.html +########################################################################################## +esp8266: + board: esp8285 + restore_from_flash: true + early_pin_init: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false + +########################################################################################## +# ESPHome LOGGING +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 # free UART for CSE7766 \ No newline at end of file diff --git a/esphome/esp-downstairsdishwasher.yaml b/esphome/esp-downstairsdishwasher.yaml new file mode 100644 index 0000000..5e8d58c --- /dev/null +++ b/esphome/esp-downstairsdishwasher.yaml @@ -0,0 +1,186 @@ +########################################################################################## +########################################################################################## +# DOWNSTAIRS DISHWASHER POWER MONITOR +# +# Controlled by a Athom Smart Plug V1 +# package_import_url: github://athom-tech/athom-configs/athom-smart-plug.yaml +# +# V1.3 2025-08-27 Changed to Athom V2 +# V1.2 2025-06-15 Changed to Athom V1 +# V1.1 2025-06-12 Tidyups and packages added +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-downstdishwasher" + friendly_name: "Downstairs Dishwasher Power" + description_comment: "Downstairs Dishwasher Power :: Athom Smart Plug Power V2" + device_area: "Downstairs Flat" # 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: "v1.3" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # Passwords (unfortunately you can't use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-downstdishwasher_ip + + # Device General Settings + log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for for general sensors etc + + # Device Specific Settings + # Athom Power Monitor Plug V2 + relay_icon: "mdi:power-socket-au" + current_limit: "10" # Current Limit in Amps. AU Plug = 10. IL, BR, EU, UK, US Plug = 16. + sensor_update_interval: "10s" + relay_restore_mode: RESTORE_DEFAULT_ON + hide_energy_sensor: "true" + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + + # Network and Wifi + 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}" + + # Network and Wifi + common_api: !include + file: common/api_common.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}" + + # Time/SNMP + 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}" + + # Device Specific included packages + # Athom Power Monitor Plug + common_athompowermonV2: !include + file: common/athompowermonv2_common.yaml + vars: + local_friendly_name: "${friendly_name}" + local_current_limit: "${current_limit}" + sensor_update_interval: "${sensor_update_interval}" + relay_restore_mode: "${relay_restore_mode}" + power_plug_type: "${relay_icon}" + hide_energy_sensor: "${hide_energy_sensor}" + +########################################################################################## +# 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}" + on_boot: + - priority: 600 + then: + - select.set_index: + id: power_mode + index: !lambda |- + return id(restore_mode)-1; + - lambda: |- + switch(id(restore_mode)) + { + case 1:{ + id(relay).turn_off(); + break; + } + case 2:{ + id(relay).turn_on(); + break; + } + default:{ + break; + } + } + +########################################################################################## +# 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 + early_pin_init: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false + +########################################################################################## +# ESPHome LOGGING +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) + +########################################################################################## +# GLOBAL VARIABLES +# https://esphome.io/guides/automations.html?highlight=globals#global-variables +########################################################################################## +globals: + # 0 = Always_Off. 1 = Restore_Power_Off. 2 = Always_On. + - id: restore_mode + type: int + restore_value: yes + initial_value: "2" + +########################################################################################## +# SELECT COMPONENT +# https://esphome.io/components/select/index.html +########################################################################################## +select: + - platform: template + name: "Power On State" + id: "power_mode" + optimistic: true + icon: mdi:electric-switch + options: + - Always Off + - Always On + - Restore Previous State + on_value: + then: + - lambda: |- + id(restore_mode)=i+1; \ No newline at end of file diff --git a/esphome/esp-gasheater.yaml b/esphome/esp-gasheater.yaml new file mode 100644 index 0000000..48ec717 --- /dev/null +++ b/esphome/esp-gasheater.yaml @@ -0,0 +1,936 @@ +########################################################################################## +########################################################################################## +# RINNAI NEO - GAS HEATER CONTROLLER (BUTTON PRESS VIA RELAYS) +# +# Device: Sonoff DEV (ESP8266) + 3-Relay daughterboard +# Project: esp-gasheater Friendly Name: "Rinnai Neo Gas Heater" +# +# Purpose: +# - Replace RF remote. Press panel buttons with relays: +# ON/OFF = GPIO12 Flame UP = GPIO13 Flame DOWN = GPIO14 +# - Sense panel LEDs as inputs: +# RED Standby = GPIO4 BLUE Operating = GPIO5 +# - AUTO thermostat uses local setpoint entity (adjustable in HA) and room temp via MQTT. +# Setpoint also listens to MQTT and will adopt an external value when published. +# - MANUAL override with level select and step UP/DOWN buttons. +# - On power ON or boot (if already powered), wait warmup then force level 1 (7x DOWN). +# - Publish status text to /viewroad-status/gasfire -> "Operating" | "Standby" | "Off" +# +# Notes: +# - Heater manages safety, ignition and fan. We only press buttons and watch LEDs. +# - LEDs pull to 0V when ON. Inputs default to inverted=true. Flip if your Tasmota shows +# opposite logic. Pull-ups enabled on inputs. +# - To reduce click noise: press pulses are short and spaced; optional min on/off timers. +########################################################################################## +########################################################################################## +# V1.0 - 2025-07-21 First Setup (and replacement of Tasmota) +# +# NOTES: +# Command the fan with MQTT +# ${mqtt_local_command_full_topic}/speed/set 1,2,3,0,+,- +# ${mqtt_local_command_full_topic}/light/set ON,OFF +# Status of the fan is in MQTT +# ${mqtt_local_command_full_topic}/speed/state 1,2,3,0 +# ${mqtt_local_command_full_topic}/light/state ON,OFF +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-gasheater" + friendly_name: "Rinnai Neo Gas Heater" + description_comment: "7 Setting controller for Rinnai Neo Gas heater :: Sonoff DEV + Relay Board" + device_area: "Lounge" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Sonoff Technologies.Sonoff DEV" # Project Details + project_version: "v1.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # Passwords & Secrets (unfortunately you can't use substitutions inside secrets names) + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-gasheater_ip + mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic + mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic + + # 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 + + # MQTT LOCAL Controls + #mqtt_device_name: "bedroom3-ceilingfan" + #mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic we will use to command this locally without HA + #mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic we will use to view status locally without HA + + # MQTT REMOTE Controls + + # Button Naming & Icons + + # Switch/Relay Naming & Icons + #relay_icon: "mdi:lightbulb-group" + #light_1_name: "Fan Light" + #switch_2_name: "Fan 2 Relay" + #switch_3_name: "Fan 3 Relay" + #switch_4_name: "Fan 4 Relay" + + ######################################################################################## + # TIMING TUNABLES (string values as required by substitutions) + ######################################################################################## + relay_pulse_ms: "250" # length of each "button press" + min_click_gap_ms: "1200" # minimum gap between any two clicks + warmup_seconds: "60" # wait after ON before calibration/stepping + control_interval_s: "30" # AUTO control loop cadence + adjust_step_interval_s: "30" # gap between level steps in AUTO + recalibrate_interval_min: "180" # while at level 1, re-baseline every X min + temp_stale_timeout_min: "20" # turn OFF if no valid temp for this long (AUTO only) + fast_ramp_seconds: "180" # fast stepping window after turn-on + fast_ramp_step_s: "8" # step every 8s during fast-ramp + + + # Optional anti short-cycle (set to "0" to disable either one) + min_on_minutes: "5" + min_off_minutes: "5" + + ######################################################################################## + # ELECTRICAL POLARITY + ######################################################################################## + relay_inverted: "true" # many relay boards are active-low + red_led_inverted: "true" # LEDs pull to 0V when ON -> invert input + blue_led_inverted: "true" + + ######################################################################################## + # THERMOSTAT HYSTERESIS (in C) + ######################################################################################## + on_below_c: "0.5" # turn ON when temp <= setpoint - on_below_c + off_above_c: "0.3" # turn OFF when temp >= setpoint + off_above_c + + ######################################################################################## + # SETPOINT LIMITS FOR HA NUMBER (in C) + ######################################################################################## + setpoint_min_c: "6.0" + setpoint_max_c: "32.0" + setpoint_step_c: "0.1" + +########################################################################################## +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +########################################################################################## +packages: + 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}" + 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 + vars: + local_device_name: "${device_name}" + 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}" + # On boot: if heater is already powered, warm up then calibrate to level 1. + # This protects against unknown state after power outages. + on_boot: + priority: -100 + then: + - script.execute: recompute_powered_state + - lambda: |- + // Initialize min on/off timers according to current state + if (id(powered_state).state) { + if (id(last_on_ms) == 0) id(last_on_ms) = millis(); + } else { + if (id(last_off_ms) == 0) id(last_off_ms) = millis(); + } + // Suppress level adjustments during boot warmup window + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + - delay: ${warmup_seconds}s + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - logger.log: "Boot: heater already powered. Calibrating to level 1." + - script.execute: calibrate_to_min + else: + - logger.log: "Boot: heater already powered. Remote Control Only -> skipping calibration." + +# platformio_options: +# build_flags: +# - "-Os" +# - "-Wl,--gc-sections" +# - "-fno-exceptions" +# - "-fno-rtti" + + + +########################################################################################## +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +########################################################################################## +esp8266: + board: esp01_1m # The original sonoff basic + restore_from_flash: true # restore some values on reboot + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false # Disabling will make the build file smaller (and it is still available via static IP) + +########################################################################################## +# Global Variables for use in automations etc +# https://esphome.io/guides/automations.html?highlight=globals#global-variables +########################################################################################## +globals: + # Current flame level we believe the heater is at (1..7). Kept across reboots. + - id: current_level + type: int + restore_value: yes + initial_value: "1" + + # Target flame level we want to reach (set by AUTO or manual selection). Not persisted. + - id: desired_level + type: int + restore_value: no + initial_value: "1" + + # Timestamp (ms since boot) of the last level step (up/down). Used for rate limiting. + - id: last_adjust_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # Timestamp (ms) of the last calibration-to-min action. Used for periodic re-baseline. + - id: last_recal_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # Timestamp (ms) when powered_state last went ON. Used to enforce min_on_minutes. + - id: last_on_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # Timestamp (ms) when powered_state last went OFF. Used to enforce min_off_minutes. + - id: last_off_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # Local thermostat setpoint (°C). This is the authority; mirrored to MQTT with guard. + - id: setpoint_c + type: float + restore_value: yes + initial_value: "21.0" + + # Guard to avoid MQTT echo: true while applying a setpoint that came from MQTT. + - id: sp_suppress_mqtt + type: bool + restore_value: no + initial_value: "false" + + # Timestamp (ms) of last received room temperature via MQTT. Detects stale sensor. + - id: last_temp_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # Until this ms timestamp we suppress any level changes (post-ON warmup window). + - id: warmup_until_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # The level chosen on the previous AUTO tick. Used to confirm stable targets. + - id: last_level_target + type: int + restore_value: no + initial_value: "1" + + # True if desired_level matched last_level_target on consecutive ticks. + - id: target_confirmed + type: bool + restore_value: no + initial_value: "true" + + # End of the fast-ramp window (ms since boot). Allows faster stepping right after ON. + - id: ramp_until_ms + type: uint32_t + restore_value: no + initial_value: "0" + + # True when we initiated power ON ourselves (not RF/panel). Helps skip duplicate actions. + - id: self_power_on + type: bool + restore_value: no + initial_value: "false" + +########################################################################################## +# 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, Serial control) + #esp8266_store_log_strings_in_flash: false + #tx_buffer_size: 64 + +########################################################################################## +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +########################################################################################## +# GPIO MAP (PHYSICAL) +# +# INPUTS: +# GPIO4 -> RED Standby LED (binary_sensor.led_standby_red) +# GPIO5 -> BLUE Operating LED (binary_sensor.led_operating_blue) +# +# OUTPUTS (RELAY COILS - INTERNAL): +# GPIO12 -> ON/OFF button (switch.sw_power_raw) +# GPIO13 -> Flame UP button (switch.sw_up_raw) +# GPIO14 -> Flame DOWN button (switch.sw_down_raw) +########################################################################################## +binary_sensor: + - platform: gpio + id: led_standby_red + name: "${friendly_name} Standby LED" + pin: + number: GPIO4 + mode: + input: true + pullup: true + inverted: ${red_led_inverted} + filters: + - delayed_on_off: 50ms + entity_category: diagnostic + on_state: + - script.execute: publish_status + - script.execute: recompute_powered_state + + - platform: gpio + id: led_operating_blue + name: "${friendly_name} Operating LED" + pin: + number: GPIO5 + mode: + input: true + pullup: true + inverted: ${blue_led_inverted} + filters: + - delayed_on_off: 50ms + entity_category: diagnostic + on_state: + - script.execute: publish_status + - script.execute: recompute_powered_state + + # Derived powered state (any LED on). Internal helper for min on/off timing. + # NOTE: No lambda or update_interval here — state is pushed by a script. + - platform: template + id: powered_state + internal: true + on_press: + - lambda: |- + id(last_on_ms) = millis(); + // If AUTO, enforce warmup lockout on RF/panel ON too + if (id(mode_select).state == "AUTO") { + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + } + # If heater was powered ON externally (RF/panel) while in AUTO, baseline after warmup + - if: + condition: + lambda: 'return (id(mode_select).state == "AUTO") && (!id(self_power_on));' + then: + - lambda: |- + // Start fast-ramp window as well + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: auto_control_tick + - script.execute: adjust_to_desired + on_release: + - lambda: |- + id(last_off_ms) = millis(); + // Clear self-power flag on any OFF edge so the next RF ON is recognized + id(self_power_on) = false; + + +########################################################################################## +# SWITCH COMPONENT +# https://esphome.io/components/switch/ +########################################################################################## +switch: + # Raw coils (do not expose these) + - platform: gpio + id: sw_power_raw + internal: true + restore_mode: ALWAYS_OFF + pin: + number: GPIO12 + inverted: ${relay_inverted} + + - platform: gpio + id: sw_up_raw + internal: true + restore_mode: ALWAYS_OFF + pin: + number: GPIO13 + inverted: ${relay_inverted} + + - platform: gpio + id: sw_down_raw + internal: true + restore_mode: ALWAYS_OFF + pin: + number: GPIO14 + inverted: ${relay_inverted} + + # Power as a logical switch (we press ON/OFF only if a change is needed). + - platform: template + id: power_switch + name: "${friendly_name} Power" + icon: mdi:power + lambda: |- + return (id(led_operating_blue).state || id(led_standby_red).state); + turn_on_action: + - if: + condition: + lambda: |- + return !(id(led_operating_blue).state || id(led_standby_red).state); + then: + - script.execute: press_power + - script.execute: after_power_on_reset_to_auto + turn_off_action: + - if: + condition: + lambda: |- + return (id(led_operating_blue).state || id(led_standby_red).state); + then: + - script.execute: press_power + +########################################################################################## +# Sensors +# https://esphome.io/components/sensor/index.html +########################################################################################## +# MQTT I/O (ROOM TEMP IN, SETPOINT IN/OUT, STATUS TEXT OUT) +sensor: + - platform: mqtt_subscribe + id: room_temp + name: "${friendly_name} Room Temperature (MQTT)" + topic: /viewroad-status/lounge-environment/temperature + unit_of_measurement: "C" + accuracy_decimals: 1 + on_value: + - lambda: |- + id(last_temp_ms) = millis(); + float v = x; + if (v < -10.0f) v = -10.0f; + if (v > 50.0f) v = 50.0f; + if (v != id(room_temp).state) id(room_temp).publish_state(v); + + # Setpoint mirror from MQTT -> adopt into local number without echoing back + - platform: mqtt_subscribe + id: setpoint_from_mqtt + internal: true + topic: /viewroad-status/lounge-environment/temp-setpoint + unit_of_measurement: "C" + accuracy_decimals: 1 + on_value: + - lambda: |- + // Clamp to substitution-defined range + float v = x; + const float vmin = ${setpoint_min_c}; + const float vmax = ${setpoint_max_c}; + if (v < vmin) v = vmin; + if (v > vmax) v = vmax; + + // Guard: applying an MQTT-driven update -> do not re-publish + id(sp_suppress_mqtt) = true; + id(setpoint_c) = v; + - number.set: + id: setpoint_number + value: !lambda 'return id(setpoint_c);' + - lambda: |- + id(sp_suppress_mqtt) = false; + + +########################################################################################## +# TEXT SENSOR COMPONENT +# https://esphome.io/components/text_sensor/index.html +########################################################################################## +text_sensor: + - platform: template + id: status_text + name: "${friendly_name} Status Text" + update_interval: 1s + lambda: |- + if (id(led_operating_blue).state) return std::string("Operating"); + if (id(led_standby_red).state) return std::string("Standby"); + return std::string("Off"); + +########################################################################################## +# SELECT COMPONENT +# https://esphome.io/components/select/index.html +########################################################################################## +select: + # AUTO or MANUAL (MANUAL is sticky until next power ON) + - platform: template + id: mode_select + name: "${friendly_name} Mode" + options: ["AUTO","MANUAL","Remote Control Only"] + optimistic: true + initial_option: "AUTO" + restore_value: true + + # Flame level (1..7). Selecting forces MANUAL and adjusts to the chosen level. + - platform: template + id: flame_level_select + name: "${friendly_name} Flame Level" + options: ["1","2","3","4","5","6","7"] + optimistic: true + restore_value: true + set_action: + # If selection equals our known current level, ignore to avoid loops. + - if: + condition: + lambda: |- + int sel = atoi(x.c_str()); + return sel == id(current_level); + then: + - logger.log: "Flame Level: no change; ignoring selection." + else: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - select.set: { id: mode_select, option: "MANUAL" } + - lambda: |- + int target = atoi(x.c_str()); + if (target < 1) target = 1; + if (target > 7) target = 7; + id(desired_level) = target; + - if: + condition: + lambda: 'return !id(powered_state).state;' + then: + # Power on, respect warmup, calibrate to level 1, then adjust toward desired (still MANUAL) + - script.execute: press_power + - lambda: |- + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); // fast ramp window + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: adjust_to_desired + else: + - script.execute: adjust_to_desired + else: + - logger.log: "Remote Control Only: ignoring Flame Level selection." + + +################################################################################################# +# BUTTON COMPONENT +# https://esphome.io/components/button/index.html +################################################################################################# +button: + # Manual step buttons (each press forces MANUAL) - button up + - platform: template + id: btn_flame_up + name: "${friendly_name} Flame Up (step)" + on_press: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - select.set: { id: mode_select, option: "MANUAL" } + - script.execute: step_up_once + else: + - logger.log: "Remote Control Only: ignoring Flame Up (step)." + + # Manual step buttons (each press forces MANUAL) - button down + - platform: template + id: btn_flame_down + name: "${friendly_name} Flame Down (step)" + on_press: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - select.set: { id: mode_select, option: "MANUAL" } + - script.execute: step_down_once + else: + - logger.log: "Remote Control Only: ignoring Flame Down (step)." + + # Calibrate + - platform: template + id: btn_calibrate_min + name: "${friendly_name} Calibrate: Force Level 1 (7x down)" + entity_category: config + on_press: + - script.execute: calibrate_to_min + + +################################################################################################# +# NUMBER COMPONENT +# https://esphome.io/components/number/ +################################################################################################# +number: + # Setpoint number (primary authority). Two-way MQTT sync with loop guard. + - platform: template + id: setpoint_number + name: "${friendly_name} Setpoint" + icon: mdi:thermometer + min_value: ${setpoint_min_c} + max_value: ${setpoint_max_c} + step: ${setpoint_step_c} + mode: slider + lambda: |- + return id(setpoint_c); + set_action: + # 1) adopt the new value locally + - lambda: |- + id(setpoint_c) = x; + # 2) unless we are processing an incoming MQTT update, publish out + - if: + condition: + lambda: |- + return !id(sp_suppress_mqtt); + then: + - mqtt.publish: + topic: /viewroad-status/lounge-environment/temp-setpoint + retain: true + payload: !lambda |- + char buf[16]; + snprintf(buf, sizeof(buf), "%.1f", x); + return std::string(buf); + +########################################################################################## +# INTERVAL COMPONENT +# https://esphome.io/components/interval/ +########################################################################################## +interval: + # AUTO loop tick (only when AUTO is enabled) + - interval: ${control_interval_s}s + then: + - if: + condition: + lambda: 'return id(mode_select).state == "AUTO";' + then: + - script.execute: auto_control_tick + + +################################################################################################# +# SCRIPT COMPONENT +# https://esphome.io/components/script.html +################################################################################################# +script: + # Publish textual status to MQTT + - id: publish_status + mode: restart + then: + - mqtt.publish: + topic: /viewroad-status/gasfire + payload: !lambda |- + if (id(led_operating_blue).state) return std::string("Operating"); + if (id(led_standby_red).state) return std::string("Standby"); + return std::string("Off"); + + - id: recompute_powered_state + then: + - lambda: |- + const bool new_state = id(led_operating_blue).state || id(led_standby_red).state; + id(powered_state).publish_state(new_state); + + # Low-level press with enforced pulse and gap + - id: press_with_gap + mode: queued + parameters: + which: string + then: + - if: + condition: + lambda: 'return which == "power";' + then: + - switch.turn_on: sw_power_raw + - delay: ${relay_pulse_ms}ms + - switch.turn_off: sw_power_raw + else: + - if: + condition: + lambda: 'return which == "up";' + then: + - switch.turn_on: sw_up_raw + - delay: ${relay_pulse_ms}ms + - switch.turn_off: sw_up_raw + else: + - switch.turn_on: sw_down_raw + - delay: ${relay_pulse_ms}ms + - switch.turn_off: sw_down_raw + - delay: ${min_click_gap_ms}ms + + + - id: press_power + then: + - lambda: |- + id(self_power_on) = true; + - script.execute: { id: press_with_gap, which: "power" } + + + - id: press_up + then: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - script.execute: { id: press_with_gap, which: "up" } + else: + - logger.log: "Remote Control Only: ignoring UP press." + + - id: press_down + then: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - script.execute: { id: press_with_gap, which: "down" } + else: + - logger.log: "Remote Control Only: ignoring DOWN press." + + # Single-step level adjusters (track current_level and update the select) + - id: step_up_once + then: + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - lambda: |- + if (id(current_level) < 7) id(current_level)++; + - script.execute: press_up + - select.set: + id: flame_level_select + option: !lambda 'return std::to_string(id(current_level));' + - lambda: |- + id(last_adjust_ms) = millis(); + else: + - logger.log: "Heater is OFF; ignoring manual step up." + + - id: step_down_once + then: + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - lambda: |- + if (id(current_level) > 1) id(current_level)--; + - script.execute: press_down + - select.set: + id: flame_level_select + option: !lambda 'return std::to_string(id(current_level));' + - lambda: |- + id(last_adjust_ms) = millis(); + else: + - logger.log: "Heater is OFF; ignoring manual step down." + + + # Force level 1 certainty (7x DOWN) - skipped in Remote Control Only and if OFF + - id: calibrate_to_min + then: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only" && id(powered_state).state;' + then: + - repeat: + count: 7 + then: + - script.execute: press_down + - lambda: |- + id(current_level) = 1; + id(last_recal_ms) = millis(); + - select.set: + id: flame_level_select + option: "1" + else: + - logger.log: "Skipping calibration (Remote Control Only or heater OFF)." + + # Move smoothly toward desired_level (rate-limited) + - id: adjust_to_desired + mode: queued + then: + - if: + condition: + lambda: 'return millis() < id(warmup_until_ms);' + then: + - logger.log: + format: "Warmup: suppressing level change." + level: DEBUG + else: + - if: + condition: + lambda: |- + // Faster stepping during ramp window, normal otherwise + const uint32_t step_ms = (millis() < id(ramp_until_ms)) + ? (${fast_ramp_step_s} * 1000UL) + : (${adjust_step_interval_s} * 1000UL); + return (millis() - id(last_adjust_ms)) >= step_ms; + then: + - if: + condition: + lambda: 'return !id(powered_state).state;' + then: + - logger.log: + format: "Heater is OFF; not adjusting level." + level: DEBUG + else: + # Proceed if target is confirmed OR we are still in the ramp window + - if: + condition: + lambda: 'return id(target_confirmed) || (millis() < id(ramp_until_ms));' + then: + - if: + condition: + lambda: 'return id(current_level) < id(desired_level);' + then: + - script.execute: step_up_once + else: + - if: + condition: + lambda: 'return id(current_level) > id(desired_level);' + then: + - script.execute: step_down_once + + + - id: after_power_on_reset_to_auto + then: + - if: + condition: + lambda: 'return id(mode_select).state != "Remote Control Only";' + then: + - lambda: |- + // Suppress level adjustments during post-power-on warmup + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + // Enable a short fast-ramp window for quicker heat-up + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); + - select.set: { id: mode_select, option: "AUTO" } + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + # Immediately compute a level and move toward it (no extra 30s wait) + - script.execute: auto_control_tick + - script.execute: adjust_to_desired + - lambda: |- + // Done with our own ON sequence + id(self_power_on) = false; + else: + - logger.log: "Remote Control Only: keeping mode; no auto-calibrate." + + + # AUTO thermostat logic (hysteresis + level mapping) + - id: auto_control_tick + mode: queued + then: + - lambda: |- + const float t = id(room_temp).state; + const float sp = id(setpoint_c); + const uint32_t now_ms = millis(); + + // Fail-safe: if temperature is missing (NAN), optionally turn OFF after timeout. + if (isnan(t)) { + const bool powered = id(powered_state).state; + const bool stale = (id(last_temp_ms) > 0) && + ((now_ms - id(last_temp_ms)) >= (${temp_stale_timeout_min} * 60000UL)); + if (powered && stale) { + ESP_LOGW("gas", "AUTO: temperature stale -> turning OFF as fail-safe"); + id(press_power).execute(); + } else { + ESP_LOGW("gas", "AUTO: temperature missing; skipping control tick"); + } + return; + } + + if (isnan(sp)) { + ESP_LOGW("gas", "AUTO: setpoint missing; skipping control tick"); + return; + } + + const bool powered = id(powered_state).state; + const float low = sp - ${on_below_c}; + const float high = sp + ${off_above_c}; + + // Honour minimum on/off durations + const bool can_turn_on = (!powered) && ((${min_off_minutes} == 0) || ((now_ms - id(last_off_ms)) >= (${min_off_minutes} * 60000UL))); + const bool can_turn_off = ( powered) && ((${min_on_minutes} == 0) || ((now_ms - id(last_on_ms)) >= (${min_on_minutes} * 60000UL))); + + // Hysteresis-based ON/OFF + if (t <= low && can_turn_on) { + ESP_LOGI("gas", "AUTO: below setpoint -> ON"); + id(press_power).execute(); + id(after_power_on_reset_to_auto).execute(); + return; + } + if (t >= high && can_turn_off) { + ESP_LOGI("gas", "AUTO: above setpoint -> OFF"); + id(press_power).execute(); + return; + } + + // Map error to desired flame level when powered. + // Always set desired_level; use target_confirmed to gate normal (non-ramp) steps. + if (powered) { + float diff = sp - t; // positive => need more heat + int target = 1; + if (diff > 3.0f) target = 7; + else if (diff > 2.0f) target = 6; + else if (diff > 1.5f) target = 5; + else if (diff > 1.0f) target = 4; + else if (diff > 0.6f) target = 3; + else if (diff > 0.2f) target = 2; + else target = 1; + + id(desired_level) = target; + id(target_confirmed) = (target == id(last_level_target)); + id(last_level_target) = target; + } + + + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - if: + condition: + lambda: |- + return (id(desired_level) == 1) && + ((millis() - id(last_recal_ms)) >= (${recalibrate_interval_min} * 60000UL)); + then: + - logger.log: "Re-baseline at level 1 (7x DOWN)." + - script.execute: calibrate_to_min + - script.execute: adjust_to_desired + + diff --git a/esphome/esp-poollightpower.yaml b/esphome/esp-poollightpower.yaml index b88c45a..4753079 100644 --- a/esphome/esp-poollightpower.yaml +++ b/esphome/esp-poollightpower.yaml @@ -6,6 +6,8 @@ # dashboard_import: # package_import_url: github://athom-tech/esp32-configs/athom-smart-plug.yaml # +# 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 @@ -62,7 +64,7 @@ substitutions: # Project Naming project_name: "Athom Technology.Smart Plug V1" # Project Details - project_version: "v2.4" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + 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 @@ -83,6 +85,18 @@ substitutions: 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: "0" + ########################################################################################## # PACKAGES: Included Common Packages # https://esphome.io/components/packages.html @@ -249,6 +263,57 @@ globals: 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 @@ -260,9 +325,6 @@ text_sensor: ############################ #################################################### # 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" @@ -363,8 +425,6 @@ text_sensor: #################################################### # Subscribe to operation mode: # OFF, ON, TIMER, BOOST - # We do case-insensitive compare using strcasecmp - # (Requires typically included in ESPHome) #################################################### # MQTT subscription: set mode, then immediately re-evaluate relay - platform: mqtt_subscribe @@ -430,7 +490,6 @@ text_sensor: 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); @@ -445,7 +504,6 @@ text_sensor: 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); @@ -460,7 +518,6 @@ text_sensor: 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); @@ -487,7 +544,7 @@ binary_sensor: then: - lambda: |- if (id(relay).state) { - // Relay is ON: turn it OFF and set mode to 0 (TIMER) + // Relay is ON: turn it OFF and set mode to 2 (TIMER) id(relay).turn_off(); id(operation_mode) = 2; } else { @@ -564,6 +621,23 @@ switch: 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 @@ -635,46 +709,280 @@ select: } - 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: evaluate and drive the relay 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); - // BOOST just forces the relay on - if (mode == 3) { - id(relay).turn_on(); - return; - } - - // OFF → always off - if (mode == 0) { - id(relay).turn_off(); - return; - } - - // ON → always on - if (mode == 1) { - id(relay).turn_on(); - return; - } - - // TIMER → follow schedule windows - { + 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; - if (should_on) id(relay).turn_on(); - else id(relay).turn_off(); + 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 @@ -702,4 +1010,3 @@ interval: } } - script.execute: evaluate_relay_state - diff --git a/packages/ha_summary.yaml b/packages/ha_summary.yaml new file mode 100644 index 0000000..7f36e20 --- /dev/null +++ b/packages/ha_summary.yaml @@ -0,0 +1,236 @@ +# ===================================================================== +# Home Assistant Summary - package (ESPHome, Tasmota, ZHA, Zigbee2MQTT) +# Robust Z2M detection using device identifiers containing "zigbee2mqtt". +# ===================================================================== + +template: + - trigger: + - platform: event + event_type: homeassistant_started + - platform: event + event_type: entity_registry_updated + - platform: event + event_type: device_registry_updated + - platform: event + event_type: area_registry_updated + - platform: event + event_type: core_config_updated + - platform: time_pattern + minutes: "/5" + + sensor: + # ---------- Totals and basics ---------- + - name: HA Summary - entities total + unique_id: ha_summary_entities_total + state: "{{ states | list | length }}" + unit_of_measurement: "entities" + + - name: HA Summary - devices total + unique_id: ha_summary_devices_total + state: >- + {% set ids = namespace(v=[]) %} + {% for s in states %} + {% set did = device_id(s.entity_id) %} + {% if did and did not in ids.v %} + {% set ids.v = ids.v + [did] %} + {% endif %} + {% endfor %} + {{ ids.v | length }} + unit_of_measurement: "devices" + + - name: HA Summary - entities broken + unique_id: ha_summary_entities_broken + state: >- + {{ states | selectattr('state','in',['unavailable','unknown']) | list | length }} + unit_of_measurement: "entities" + + - name: HA Summary - cameras + unique_id: ha_summary_cameras + state: "{{ states.camera | list | length }}" + unit_of_measurement: "cameras" + + - name: HA Summary - areas + unique_id: ha_summary_areas + state: "{{ areas() | list | length }}" + unit_of_measurement: "areas" + + - name: HA Summary - scenes + unique_id: ha_summary_scenes + state: "{{ states.scene | list | length }}" + + - name: HA Summary - scripts + unique_id: ha_summary_scripts + state: "{{ states.script | list | length }}" + + - name: HA Summary - automations + unique_id: ha_summary_automations + state: "{{ states.automation | list | length }}" + + # ---------- ESPHome ---------- + - name: HA Summary - esphome entities + unique_id: ha_summary_esphome_entities + state: "{{ integration_entities('esphome') | list | length }}" + + - name: HA Summary - esphome devices + unique_id: ha_summary_esphome_devices + state: >- + {% set ids = namespace(v=[]) %} + {% for e in integration_entities('esphome') %} + {% set did = device_id(e) %} + {% if did and did not in ids.v %} + {% set ids.v = ids.v + [did] %} + {% endif %} + {% endfor %} + {{ ids.v | length }} + + # ---------- Tasmota ---------- + - name: HA Summary - tasmota entities + unique_id: ha_summary_tasmota_entities + state: "{{ integration_entities('tasmota') | list | length }}" + + - name: HA Summary - tasmota devices + unique_id: ha_summary_tasmota_devices + state: >- + {% set ids = namespace(v=[]) %} + {% for e in integration_entities('tasmota') %} + {% set did = device_id(e) %} + {% if did and did not in ids.v %} + {% set ids.v = ids.v + [did] %} + {% endif %} + {% endfor %} + {{ ids.v | length }} + + # ---------- Zigbee via ZHA ---------- + - name: HA Summary - zha entities + unique_id: ha_summary_zha_entities + state: "{{ integration_entities('zha') | list | length }}" + + - name: HA Summary - zha devices + unique_id: ha_summary_zha_devices + state: >- + {% set ids = namespace(v=[]) %} + {% for e in integration_entities('zha') %} + {% set did = device_id(e) %} + {% if did and did not in ids.v %} + {% set ids.v = ids.v + [did] %} + {% endif %} + {% endfor %} + {{ ids.v | length }} + + # ---------- Zigbee via Zigbee2MQTT (robust) ---------- + # Match if device identifiers mention "zigbee2mqtt". + # Fallbacks: entity_id or unique_id contains "zigbee2mqtt" or ".z2m_". + - name: HA Summary - z2m entities + unique_id: ha_summary_z2m_entities + state: >- + {% set ent = namespace(v=[]) %} + {% for s in states %} + {% set eid = s.entity_id %} + {% set did = device_id(eid) %} + {% set match = false %} + {% if did %} + {% set ident = device_attr(did,'identifiers') | default([], true) %} + {% if (ident | string | lower) | regex_search('zigbee2mqtt') %} + {% set match = true %} + {% endif %} + {% endif %} + {% if not match %} + {% set uid = state_attr(eid,'unique_id') | default('', true) | string | lower %} + {% if 'zigbee2mqtt' in eid or '.z2m_' in eid or 'zigbee2mqtt' in uid %} + {% set match = true %} + {% endif %} + {% endif %} + {% if match %} + {% set ent.v = ent.v + [eid] %} + {% endif %} + {% endfor %} + {{ ent.v | unique | list | length }} + + - name: HA Summary - z2m devices + unique_id: ha_summary_z2m_devices + state: >- + {% set dids = namespace(v=[]) %} + {% for s in states %} + {% set eid = s.entity_id %} + {% set did = device_id(eid) %} + {% if did and did not in dids.v %} + {% set ident = device_attr(did,'identifiers') | default([], true) %} + {% set uid = state_attr(eid,'unique_id') | default('', true) | string | lower %} + {% if (ident | string | lower) | regex_search('zigbee2mqtt') or + 'zigbee2mqtt' in eid or '.z2m_' in eid or 'zigbee2mqtt' in uid %} + {% set dids.v = dids.v + [did] %} + {% endif %} + {% endif %} + {% endfor %} + {{ dids.v | length }} + + # ---------- Cloud devices/entities (edit list as desired) ---------- + - name: HA Summary - cloud entities + unique_id: ha_summary_cloud_entities + state: >- + {% set list_all = + integration_entities('tuya') + + integration_entities('ewelink') + + integration_entities('meross_cloud') + + integration_entities('tapo') + + integration_entities('tplink') + + integration_entities('nest') + + integration_entities('ring') + + integration_entities('arlo') + + integration_entities('wyze') + + integration_entities('myq') + + integration_entities('nuki') + + integration_entities('august') + + integration_entities('switchbot') + + integration_entities('icloud') + %} + {{ list_all | unique | list | length }} + + - name: HA Summary - cloud devices + unique_id: ha_summary_cloud_devices + state: >- + {% set dids = namespace(v=[]) %} + {% for e in ( + integration_entities('tuya') + + integration_entities('ewelink') + + integration_entities('meross_cloud') + + integration_entities('tapo') + + integration_entities('tplink') + + integration_entities('nest') + + integration_entities('ring') + + integration_entities('arlo') + + integration_entities('wyze') + + integration_entities('myq') + + integration_entities('nuki') + + integration_entities('august') + + integration_entities('switchbot') + + integration_entities('icloud') + ) | unique %} + {% set did = device_id(e) %} + {% if did and did not in dids.v %} + {% set dids.v = dids.v + [did] %} + {% endif %} + {% endfor %} + {{ dids.v | length }} + + # ---------- Supervisor add-ons (approximate) ---------- + # Union of: update entities in hassio integration + updates attributed to Supervisor. + - name: HA Summary - addons + unique_id: ha_summary_addons + icon: mdi:puzzle + state: >- + {% set a = states.update | selectattr('entity_id','in', integration_entities('hassio')) | list %} + {% set b = states.update | selectattr('attributes.attribution','search','Supervisor') | list %} + {{ (a + b) | unique | list + | rejectattr('entity_id','search','^update\\.home_assistant_(core|supervisor|operating_system)_update$') + | list | length }} + + # ---------- HACS updates available (proxy only) ---------- + - name: HA Summary - hacs updates + unique_id: ha_summary_hacs_updates + icon: mdi:download + state: >- + {{ states.update + | selectattr('state','eq','on') + | rejectattr('attributes.in_progress','true') + | list | length }}