various tidyups and esphome adds

This commit is contained in:
root
2026-02-23 21:01:07 +13:00
parent 83e4040b84
commit 61b0c64a1e
28 changed files with 4052 additions and 3808 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ wifi:
subnet: ${static_ip_subnet}
dns1: ${static_ip_dns1}
dns2: ${static_ip_dns2}
use_address: ${local_static_ip_address}
use_address: ${local_current_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}
password: ${fallback_ap_password}
+5
View File
@@ -34,6 +34,10 @@ substitutions:
mqtt_command_main_topic: !secret mqtt_command_main_topic
mqtt_status_main_topic: !secret mqtt_status_main_topic
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# MQTT LOCAL Controls
mqtt_local_device_name: "3dprinter-power"
mqtt_local_command_topic: "${mqtt_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA
@@ -56,6 +60,7 @@ packages:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
+9 -31
View File
@@ -25,6 +25,10 @@ substitutions:
ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-arcademachine1_ip
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# Device Settings
log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "10s" # update time for for general sensors etc
@@ -49,6 +53,7 @@ packages:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
##################################################
# OPTIONAL packages (comment if you don't want them)
@@ -62,8 +67,8 @@ packages:
local_api_key: "${api_key}"
#### WEB PORTAL ####
#common_webportal: !include
# file: common/webportal_common.yaml
common_webportal: !include
file: common/webportal_common.yaml
#### MQTT ####
common_mqtt: !include
@@ -124,8 +129,8 @@ esp8266:
preferences:
flash_write_interval: 10min
#mdns:
# disabled: True # binary size saving
mdns:
disabled: False # binary size saving
##########################################################################################
# ESPHome Logging Enable
@@ -137,33 +142,6 @@ logger:
#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"
####################################################
# 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"
##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
-992
View File
@@ -1,992 +0,0 @@
##########################################################################################
##########################################################################################
#
##########################################################################################
##########################################################################################
##########################################################################################
# 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-athomv1temp1"
friendly_name: "esp-athomv1temp1"
description_comment: "esp-athomv1temp1 :: 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: "v1.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
# Passwords
api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names
ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-athomv1temp1_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/esp-athomv1temp1" # Topics you will use to change stuff
boost_duration_default: "180" # Minutes to stay ON in BOOST mode before reverting to TIMER
morning_on_default: "450" # Default in minutes from midnight. Default 07:30 => 450
morning_off_default: "450" # Default in minutes from midnight. Default 07:30 => 450 (same as ON as no need for morning schedule)
evening_on_default: "1140" # Default in minutes from midnight. Default 19:00 => 1140
evening_off_default: "1350" # Default in minutes from midnight. Default 22:30 => 1350 => 1440 is midnight
##############################################################################
# POOL LIGHT MODE CONTROL (new)
##############################################################################
max_modes: "16" # total number of modes in the cycle
mode_pulse_off: "400ms" # short OFF between quick pulses
mode_pulse_on: "400ms" # short ON after each quick pulse
mode_reset_off: "3s" # long OFF to force hardware reset to mode 1
mode_reset_window_ms: "3000" # treat OFF>=this ms as a reset-to-1 when turning back ON
mode_prime_on: "800ms" # if relay is OFF when a color is selected, turn ON for this long before pulsing
# Ignored modes bitmask: bit (mode_index-1). For 4 and 5 ignored -> (1<<3)|(1<<4) = 24.
ignored_modes_mask: "8288" # Ignore 6(32), 7(64), 14(8192) :: 32 + 64 + 8192 = 8288
##########################################################################################
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
##########################################################################################
packages:
##################################################
# MANDATORY packages (won't compile without them!)
##################################################
#### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
common_wifi: !include
file: common/network_common.yaml
vars:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
##################################################
# OPTIONAL packages (comment if you don't want them)
##################################################
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
file: common/api_common.yaml
#file: common/api_common_noencryption.yaml
vars:
local_api_key: "${api_key}"
#### WEB PORTAL ####
#common_webportal: !include
# file: common/webportal_common.yaml
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### SNTP (Only use if you want/need accurate timeclocks) ####
common_sntp: !include
file: common/sntp_common.yaml
#### DIAGNOSTICS Lite Sensors ####
common_lite_sensors: !include
file: common/sensors_common_lite.yaml
#### DIAGNOSTICS General Sensors ####
common_general_sensors: !include
file: common/sensors_common.yaml
#### DEBUGGING Sensors ####
# A count of various resets
#common_resetcount_debugging: !include
# file: common/resetcount_common.yaml
# Device Specific included packages
common_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"
##############################################################################
# POOL LIGHT MODE CONTROL GLOBALS (new)
##############################################################################
# Current assumed pool-light mode (0..max_modes). 0 means relay OFF.
- id: current_mode
type: int
restore_value: false
initial_value: "1"
# Total modes in the cycle (edit in substitutions).
- id: max_modes
type: int
restore_value: false
initial_value: "${max_modes}"
# Flag while we are actively pulsing to change modes (prevents scheduler interference).
- id: mode_changing
type: bool
restore_value: false
initial_value: "false"
# Timestamp of last relay off (for delta measurement).
- id: last_off_ms
type: uint32_t
restore_value: false
initial_value: "0"
# Millisecond threshold that separates "quick pulse" vs "reset to mode 1".
- id: reset_window_ms
type: int
restore_value: false
initial_value: "${mode_reset_window_ms}"
# Desired target mode for the cycle script.
- id: desired_mode
type: int
restore_value: false
initial_value: "1"
# Bitmask of ignored modes (1..max_modes). Example: ignore 4 & 5 -> 24.
- id: ignored_mask
type: int
restore_value: false
initial_value: "${ignored_modes_mask}"
# Internal: whether a recheck is already scheduled for min OFF enforcement.
- id: recheck_pending
type: bool
restore_value: false
initial_value: "false"
##########################################################################################
# Text Sensors
# https://esphome.io/components/text_sensor/index.html
##########################################################################################
text_sensor:
############################
# MQTT Subscriptions
############################
####################################################
# Subscribe to the Morning On time, format "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Morning On Time Setting"
id: morning_on_topic
topic: "${mqtt_timer_topic}/morning-on" # Stored in the format HH:MM
internal: True
on_value:
then:
- lambda: |-
// Expect "HH:MM" => total length = 5, with ':'
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str()); // "HH"
int minute = atoi(x.substr(3, 2).c_str()); // "MM"
id(morning_on) = hour * 60 + minute;
ESP_LOGI("timer","Received new Morning On: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Morning On format: %s", x.c_str());
}
####################################################
# Morning Off time => "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Morning Off Time Setting"
id: morning_off_topic
topic: "${mqtt_timer_topic}/morning-off" # Stored in the format HH:MM
internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str());
int minute = atoi(x.substr(3, 2).c_str());
id(morning_off) = hour * 60 + minute;
ESP_LOGI("timer","Received new Morning Off: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Morning Off format: %s", x.c_str());
}
####################################################
# Evening On time => "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Evening On Time Setting"
id: evening_on_topic
topic: "${mqtt_timer_topic}/evening-on" # Stored in the format HH:MM
internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str());
int minute = atoi(x.substr(3, 2).c_str());
id(evening_on) = hour * 60 + minute;
ESP_LOGI("timer","Received new Evening On: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Evening On format: %s", x.c_str());
}
####################################################
# Evening Off time => "HH:MM"
####################################################
- platform: mqtt_subscribe
name: "Evening Off Time Setting"
id: evening_off_topic
topic: "${mqtt_timer_topic}/evening-off" # Stored in the format HH:MM
internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (x.size() == 5 && x[2] == ':') {
int hour = atoi(x.substr(0, 2).c_str());
int minute = atoi(x.substr(3, 2).c_str());
id(evening_off) = hour * 60 + minute;
ESP_LOGI("timer","Received new Evening Off: %02d:%02d", hour, minute);
} else {
ESP_LOGW("timer","Invalid Evening Off format: %s", x.c_str());
}
####################################################
# Boost duration => 1 - 1439
####################################################
- platform: mqtt_subscribe
name: "Boost Duration"
id: boost_time_topic
topic: "${mqtt_timer_topic}/boost-time" # Stored as an integer from 1-1439
internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
// parse as integer
char *endptr;
long v = strtol(x.c_str(), &endptr, 10);
// invalid if nothing parsed, trailing chars, or out of 01439
if (endptr == x.c_str() || *endptr != '\0' || v < 0 || v > 1439) {
ESP_LOGE("boost_time", "Invalid boost_time '%s'", x.c_str());
} else {
id(boost_duration) = static_cast<int>(v);
}
####################################################
# Subscribe to operation mode:
# OFF, ON, TIMER, BOOST
####################################################
# MQTT subscription: set mode, then immediately re-evaluate relay
- platform: mqtt_subscribe
id: timer_operation_mode_topic
topic: "${mqtt_timer_topic}/operation"
internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value
on_value:
then:
- lambda: |-
if (strcasecmp(x.c_str(), "TIMER") == 0) {
id(operation_mode) = 2;
ESP_LOGI("timer","Operation mode set to TIMER");
} else if (strcasecmp(x.c_str(), "ON") == 0) {
id(operation_mode) = 1;
ESP_LOGI("timer","Operation mode set to ON");
} else if (strcasecmp(x.c_str(), "OFF") == 0) {
id(operation_mode) = 0;
ESP_LOGI("timer","Operation mode set to OFF");
} else if (strcasecmp(x.c_str(), "BOOST") == 0) {
id(operation_mode) = 3;
id(boost_timer) = 0;
ESP_LOGI("timer","Operation mode set to BOOST");
} else {
ESP_LOGW("timer","Invalid operation mode: %s", x.c_str());
}
- script.execute: evaluate_relay_state
######################################################
# Expose the current operation mode (OFF, ON, TIMER, BOOST)
######################################################
- platform: template
name: "Operation Mode State"
lambda: |-
// 0=OFF, 1=ON, 2=TIMER, 3=BOOST
switch (id(operation_mode)) {
case 0: return {"OFF"};
case 1: return {"ON"};
case 2: return {"TIMER"};
case 3: return {"BOOST"};
default: return {"UNKNOWN"};
}
update_interval: 5s
######################################################
# Expose the "Morning On" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Morning On Time"
lambda: |-
int hour = id(morning_on) / 60;
int minute = id(morning_on) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Morning Off" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Morning Off Time"
lambda: |-
int hour = id(morning_off) / 60;
int minute = id(morning_off) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Evening On" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Evening On Time"
lambda: |-
int hour = id(evening_on) / 60;
int minute = id(evening_on) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Evening Off" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Evening Off Time"
lambda: |-
int hour = id(evening_off) / 60;
int minute = id(evening_off) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
##########################################################################################
binary_sensor:
- platform: gpio
pin:
number: GPIO03
mode: INPUT_PULLUP
inverted: true
name: "Power Button"
id: power_button
filters:
- delayed_on: 20ms
on_click:
- min_length: 20ms
max_length: 500ms
then:
- lambda: |-
if (id(relay).state) {
// Relay is ON: turn it OFF and set mode to 2 (TIMER)
id(relay).turn_off();
id(operation_mode) = 2;
} else {
// Relay is OFF: turn it ON and set mode to 3 (BOOST)
id(relay).turn_on();
id(operation_mode) = 3;
}
- 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}"
# POOL LIGHT MODE CONTROL: detect quick vs long off periods and keep current_mode in sync
on_turn_off:
- lambda: |-
id(last_off_ms) = millis();
if (!id(mode_changing)) id(current_mode) = 0; // reflect OFF in select when off normally
on_turn_on:
- lambda: |-
if (id(mode_changing)) {
return;
}
uint32_t delta = millis() - id(last_off_ms);
if (delta >= (uint32_t)id(reset_window_ms)) {
id(current_mode) = 1;
} else {
id(current_mode)++;
if (id(current_mode) > id(max_modes)) id(current_mode) = 1; // keep internal index sane
}
#################################################################################################
# BUTTON COMPONENT
# https://esphome.io/components/button/index.html
#################################################################################################
button:
- platform: template
name: "Boost now"
id: boost_button
icon: "mdi:play-circle-outline"
on_press:
# 1) reset BOOST timer and set mode
- lambda: |-
id(boost_timer) = 0;
id(operation_mode) = 3;
# 2) immediately re-evaluate relay state
- script.execute: evaluate_relay_state
- platform: template
name: "Default timer settings"
id: default_timer_settings_button
icon: "mdi:restore"
on_press:
- lambda: |-
// Restore all timing globals to their YAML defaults
id(morning_on) = ${morning_on_default};
id(morning_off) = ${morning_off_default};
id(evening_on) = ${evening_on_default};
id(evening_off) = ${evening_off_default};
id(boost_duration)= ${boost_duration_default};
// Reset mode to TIMER and clear any running boost
id(operation_mode)= 2;
id(boost_timer) = 0;
ESP_LOGI("timer","Default timer settings applied");
- script.execute: evaluate_relay_state
#################################################################################################
# SELECT COMPONENT
# https://esphome.io/components/select/index.html
#################################################################################################
select:
- platform: template
name: "Operation Mode"
id: operation_mode_select
update_interval: 5s
options:
- "OFF"
- "ON"
- "TIMER"
- "BOOST"
# show the current mode
lambda: |-
switch (id(operation_mode)) {
case 1: return std::string("ON");
case 2: return std::string("TIMER");
case 3: return std::string("BOOST");
default: return std::string("OFF");
}
# when changed in HA, set mode & re-evaluate
set_action:
- lambda: |-
if (x == "OFF") { id(operation_mode) = 0; }
else if (x == "ON") { id(operation_mode) = 1; }
else if (x == "TIMER") { id(operation_mode) = 2; }
else { // BOOST
id(boost_timer) = 0;
id(operation_mode) = 3;
}
- script.execute: evaluate_relay_state
##############################################################################
# POOL LIGHT MODE SELECT (new)
# 0=OFF -> immediately turns relay OFF and sets Operation Mode = TIMER.
# Selecting a color (>=1) forces Operation Mode = ON.
# If target < current, do a long reset to mode 1, then pulse forward.
# Ignored modes are hidden here but still counted by pulses.
##############################################################################
##############################################################################
# POOL LIGHT MODE SELECT (updated display logic)
# 0=OFF -> immediately turns relay OFF and sets Operation Mode = TIMER.
# Selecting a color (>=1) forces Operation Mode = ON.
# If target < current, do a long reset to mode 1, then pulse forward.
# Ignored modes are hidden here but still counted by pulses.
##############################################################################
- platform: template
name: "Pool Light Mode"
id: pool_light_mode_select
update_interval: 250ms
# Options include all 1..16 (no ignored modes currently) plus 0=OFF
options:
- "0= OFF"
- "1= Dark Blue"
- "2= Red"
- "3= Green"
- "4= Purple"
- "5= Aqua"
#- "6= Lime"
#- "7= Cool White"
- "8= Speed Up"
- "9= Transformer"
- "10= Blurple"
- "11= Redlime"
- "12= Greenqua"
- "13= Bluequa"
#- "14= Grelime"
- "15= Redurple"
- "16= RGB Mad"
# Show OFF only when not in a color-change sequence.
# While changing, reflect the in-flight/current mode (never 0).
lambda: |-
auto label_for = [](int m) -> std::string {
switch (m) {
case 0: return "0= OFF";
case 1: return "1= Dark Blue";
case 2: return "2= Red";
case 3: return "3= Green";
case 4: return "4= Purple";
case 5: return "5= Aqua";
case 6: return "6= Lime";
case 7: return "7= Cool White";
case 8: return "8= Speed Up";
case 9: return "9= Transformer";
case 10: return "10= Blurple";
case 11: return "11= Redlime";
case 12: return "12= Greenqua";
case 13: return "13= Bluequa";
case 14: return "14= Grelime";
case 15: return "15= Redurple";
case 16: return "16= RGB Mad";
default: return "1= Dark Blue";
}
};
auto is_ignored = [&](int m) -> bool {
if (m < 1 || m > id(max_modes)) return true;
int bit = 1 << (m - 1);
return (id(ignored_mask) & bit) != 0;
};
// While stepping (mode_changing), don't show OFF even if relay is briefly OFF.
if (id(mode_changing)) {
int cm = id(current_mode);
if (cm <= 0) cm = 1; // during long reset, assume Mode 1 is next
// Skip ignored modes for display if ever configured later
if (!is_ignored(cm)) return label_for(cm);
// find next non-ignored just for display robustness
for (int m = cm + 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
for (int m = 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
return label_for(1);
}
// Normal (not changing): show OFF if relay is actually OFF, else show current mode
if (!id(relay).state) return std::string("0= OFF");
int cm = id(current_mode);
if (cm == 0) return std::string("0= OFF");
if (!is_ignored(cm)) return label_for(cm);
for (int m = cm + 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
for (int m = 1; m <= id(max_modes); m++) if (!is_ignored(m)) return label_for(m);
return label_for(1);
set_action:
# First, set flags based on selection
- lambda: |-
int target = atoi(x.c_str());
if (target < 0) target = 0;
if (target > id(max_modes)) target = id(max_modes);
if (target == 0) {
// MODE 0: force everything OFF; UI goes back to TIMER control
id(mode_changing) = false;
id(recheck_pending) = false;
id(operation_mode) = 2; // TIMER
id(current_mode) = 0; // reflect OFF in dropdown
} else {
// defensive: ignore choosing an ignored mode (options already hide them)
int bit = 1 << (target - 1);
if ((id(ignored_mask) & bit) != 0) {
ESP_LOGW("mode","Selected target %d is ignored; ignoring request", target);
return;
}
// Color selection: force ON mode and start changer
id(operation_mode) = 1;
id(desired_mode) = target;
id(mode_changing) = true;
}
# If OFF was selected, hard-stop any running scripts and cut power
- if:
condition:
lambda: 'return atoi(x.c_str()) == 0;'
then:
- script.stop: change_to_desired_mode
- script.stop: reset_to_mode1
- script.stop: enforce_off_guard
- switch.turn_off: relay
else:
- script.execute: change_to_desired_mode
#################################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
#################################################################################################
script:
# Core: evaluate and drive the relay (with enforced min OFF window)
- id: evaluate_relay_state
then:
- lambda: |-
if (id(mode_changing)) {
// do not fight the pulser
return;
}
// Determine target state
bool target_on = false;
int mode = id(operation_mode);
if (mode == 3) { // BOOST
target_on = true;
} else if (mode == 0) { // OFF
target_on = false;
} else if (mode == 1) { // ON
target_on = true;
} else if (mode == 2) { // TIMER
bool should_on = false;
if (id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off))
should_on = true;
if (id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off))
should_on = true;
target_on = should_on;
}
// Enforce minimum OFF time when turning ON normally (not during color pulses)
if (target_on) {
uint32_t delta = millis() - id(last_off_ms);
if (delta < (uint32_t) id(reset_window_ms)) {
// Still inside OFF window: wait; schedule a recheck if not already
if (!id(recheck_pending)) {
id(enforce_off_guard).execute();
}
return;
} else {
id(relay).turn_on();
return;
}
} else {
id(relay).turn_off();
return;
}
# Poll until OFF window elapsed, then re-evaluate (does not toggle the relay itself)
- id: enforce_off_guard
mode: restart
then:
- lambda: |-
id(recheck_pending) = true;
- while:
condition:
lambda: |-
if (id(mode_changing)) return false; // abort if color change starts
uint32_t delta = millis() - id(last_off_ms);
return delta < (uint32_t) id(reset_window_ms);
then:
- delay: 200ms
- lambda: |-
id(recheck_pending) = false;
- script.execute: evaluate_relay_state
# Long reset to Mode 1
- id: reset_to_mode1
mode: restart
then:
- switch.turn_off: relay
- delay: ${mode_reset_off}
- switch.turn_on: relay
- delay: ${mode_pulse_on}
- lambda: |-
id(current_mode) = 1;
# Change to desired_mode, robust from OFF and without wrap on lower targets
# Change to desired_mode, robust from OFF and without wrap on lower targets
- id: change_to_desired_mode
mode: restart
then:
# If relay is currently OFF, establish a known starting point
- if:
condition:
lambda: 'return !id(relay).state;'
then:
- lambda: |-
uint32_t delta = millis() - id(last_off_ms);
// If OFF < reset window, enforce a true reset first for predictable start
if (delta < (uint32_t) id(reset_window_ms)) {
id(reset_to_mode1).execute();
}
- if:
condition:
lambda: 'return !id(relay).state;'
then:
- switch.turn_on: relay
- delay: ${mode_prime_on}
- lambda: |-
// If we turned ON after ≥ reset window, hardware is at Mode 1
if ((millis() - id(last_off_ms)) >= (uint32_t) id(reset_window_ms)) {
id(current_mode) = 1;
}
# If target is below current, do a long reset to Mode 1, then proceed
- if:
condition:
lambda: |-
return id(desired_mode) < id(current_mode);
then:
- script.execute: reset_to_mode1
- script.wait: reset_to_mode1
# If already exactly at target, ensure ON and finish
- if:
condition:
lambda: 'return id(desired_mode) == id(current_mode);'
then:
- switch.turn_on: relay
- lambda: |-
id(mode_changing) = false;
# Step forward until we reach desired (ignored modes still counted)
- while:
condition:
lambda: |-
// Allow immediate cancellation if OFF is selected mid-flight
return id(mode_changing) && (id(current_mode) < id(desired_mode));
then:
- switch.turn_off: relay
- delay: ${mode_pulse_off}
- switch.turn_on: relay
- delay: ${mode_pulse_on}
- lambda: |-
id(current_mode)++;
# End: make sure we leave the light ON and clear the guard
- switch.turn_on: relay
- lambda: |-
id(mode_changing) = false;
#################################################################################################
# INTERVAL COMPONENT
# https://esphome.io/components/interval.html
#################################################################################################
# Interval: bumps time (even if no SNTP), then calls the script to evaluate relay state
interval:
- interval: "1min"
then:
- lambda: |-
// — update current_mins via SNTP or fallback
if (!id(sntp_time).now().is_valid()) {
id(current_mins)++;
if (id(current_mins) >= 1440) id(current_mins) = 0;
} else {
auto now = id(sntp_time).now();
id(current_mins) = now.hour * 60 + now.minute;
}
// — if in BOOST, advance boost_timer and expire when done
if (id(operation_mode) == 3) {
id(boost_timer)++;
if (id(boost_timer) >= id(boost_duration)) {
id(operation_mode) = 2;
//id(mqtt_client).publish("${mqtt_timer_topic}/operation", "TIMER");
}
}
- script.execute: evaluate_relay_state
+890
View File
@@ -0,0 +1,890 @@
##########################################################################################
##########################################################################################
# MASTER BEDROOM BEDSIDE LAMP BULB
# File: esp-bed1bedsideathombulb.yaml
#
# v1.1 - 2026-02-21 Correct GPIO mapping + use RGBCT platform (Athom LB01)
# v1.0 - 2026-02-21 Initial version (merged from preferred dimmer + Athom GPIO map)
#
# Hardware: Athom 15W E27 Bulb LB01 (RGB + CT Brightness + CT control)
#
# NOTES
# - This keeps the "preferred control" ramping / min-max / watchdog / restore logic.
# - All ramping and "Output Set (0-100)" controls FORCE the bulb into Warm White endpoint
# (so the dimming behavior mainly drives the CT Brightness channel at 3000K).
# - You can still control full RGB and CT from HA using the normal light entity.
# - Max On watchdog turns the whole bulb OFF at the time limit (all channels).
##########################################################################################
##########################################################################################
##########################################################################################
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
##########################################################################################
substitutions:
# Device Naming
device_name: "esp-bed1bedsideathombulb" # yaml file should be device_name.yaml
friendly_name: "Master Bedroom Bedside Lamp Bulb"
description_comment: "Master Bedroom Bedside lamp using RGB-CW 15W Bulb"
device_area: "Master Bedroom"
# Project Naming
project_name: "Athom 15W E27 Bulb LB01 RGBCT.Athom"
project_version: "v1.1"
# Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names)
#api_key: !secret esp-api_key
api_key: !secret esp-athombulbflasher_api_key
#ota_pass: !secret esp-ota_pass
ota_pass: !secret esp-athombulbflasher_ota_pass
static_ip_address: !secret esp-bed1bedsideathombulb_ip
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
#current_ip_address: 192.168.2.121
current_ip_address: ${static_ip_address}
# MQTT LOCAL Controls (optional local control method)
mqtt_local_device_name: "bed1-bedsidelamp"
mqtt_local_command_main_topic: !secret mqtt_command_main_topic
mqtt_local_status_main_topic: !secret mqtt_status_main_topic
mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_local_device_name}"
mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_local_device_name}"
mqtt_local_device_command_ON: "ON"
mqtt_local_device_command_OFF: "OFF"
# Device Specific Settings
log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE
update_interval: "20s"
led_gamma: "1.2"
minimum_led_output: "1" # % If at this value or below, we'll switch it completely off
maximum_led_output: "90" # % Hard cap
max_on_default_hours: "24" # 0 = no automatic turn-off
pwm_frequency: "1000" # Hz
# Warm endpoint for this bulb (spec range is 3000K-6000K)
bulb_warm_kelvin: "3000K"
bulb_cold_kelvin: "6000K"
# RGBCT endpoints must be given as mireds in the rgbct component
# 6000K = 166.7 mireds, 3000K = 333.3 mireds
bulb_cold_mireds: "167 mireds"
bulb_warm_mireds: "333 mireds"
# Athom LB01 GPIO mapping (correct)
bulb_red_pin: GPIO4
bulb_green_pin: GPIO12
bulb_blue_pin: GPIO14
bulb_ct_brightness_pin: GPIO5
bulb_ct_pin: GPIO13
##########################################################################################
# PACKAGES: Included Common Packages
##########################################################################################
packages:
#### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
common_wifi: !include
file: common/network_common.yaml
vars:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API ####
common_api: !include
#file: common/api_common.yaml
file: common/api_common_noencryption.yaml
vars:
local_api_key: "${api_key}"
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### DIAGNOSTICS Sensors ####
diag_basic: !include common/include_basic_diag_sensors.yaml
diag_more: !include common/include_more_diag_sensors.yaml
diag_debug: !include common/include_debug_diag_sensors.yaml
##########################################################################################
# ESPHome CORE CONFIGURATION
##########################################################################################
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
comment: "${description_comment}"
area: "${device_area}"
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
# Run later so everything is initialised before we touch states.
priority: 200
then:
# Keep the HA dropdown in sync with the stored mode
- lambda: |-
switch (id(restart_mode)) {
case 0: id(restart_action).publish_state("Fade up to full"); break;
case 1: id(restart_action).publish_state("Restore Brightness"); break;
case 2: default: id(restart_action).publish_state("Remain Off"); break;
}
# Mode 0: Fade up to full (obeys fade settings; forces Warm endpoint)
- if:
condition:
lambda: 'return id(restart_mode) == 0;'
then:
- lambda: 'id(ramp_switch_target_on) = true;'
- script.execute: ramp_on_script
# Mode 1: Restore Brightness (deferred; forces Warm endpoint)
- if:
condition:
lambda: 'return id(restart_mode) == 1;'
then:
- script.execute: deferred_restore_brightness
# Mode 2: Remain Off
- if:
condition:
lambda: 'return id(restart_mode) == 2;'
then:
- script.stop: ramp_on_script
- script.stop: ramp_off_script
- lambda: 'id(ramp_switch_target_on) = false;'
- light.turn_off:
id: bulb_light
transition_length: 0s
# Boot complete: allow OFF handlers again
- lambda: 'id(booting) = false;'
##########################################################################################
# ESP PLATFORM AND FRAMEWORK
##########################################################################################
esp8266:
board: esp8285
restore_from_flash: true
mdns:
disabled: true
preferences:
flash_write_interval: 2min
##########################################################################################
# GLOBAL VARIABLES
##########################################################################################
globals:
- id: min_brightness_pct
type: int
restore_value: true
initial_value: "${minimum_led_output}"
- id: max_brightness_pct
type: int
restore_value: false
initial_value: "${maximum_led_output}"
- id: max_on_hours
type: int
restore_value: true
initial_value: "${max_on_default_hours}"
- id: led_gamma_f
type: float
restore_value: false
initial_value: ${led_gamma}
- id: ramp_up_ms
type: int
restore_value: true
initial_value: "5000"
- id: ramp_down_ms
type: int
restore_value: true
initial_value: "10000"
- id: restart_mode
type: int
restore_value: true
initial_value: "0" # 0=Fade full, 1=Restore brightness, 2=Remain off
- id: ramp_switch_target_on
type: bool
restore_value: true
initial_value: "false"
- id: suppress_slider_sync
type: bool
restore_value: false
initial_value: "false"
- id: last_brightness_pct
type: float
restore_value: true
initial_value: "0.0"
- id: last_set_pos
type: int
restore_value: false
initial_value: "-1"
- id: last_ramp_ms
type: int
restore_value: false
initial_value: "0"
- id: restore_target_pct
type: float
restore_value: false
initial_value: "0.0"
- id: is_ramping
type: bool
restore_value: false
initial_value: "false"
- id: booting
type: bool
restore_value: false
initial_value: "true"
- id: ramping_for_off
type: bool
restore_value: false
initial_value: "false"
##########################################################################################
# LOGGER
##########################################################################################
logger:
level: "${log_level}"
baud_rate: 0
##########################################################################################
# MQTT COMMANDS (adds device-specific triggers to common MQTT)
##########################################################################################
mqtt:
on_message:
- topic: "${mqtt_local_command_topic}/light/set"
payload: "${mqtt_local_device_command_ON}"
then:
- switch.turn_on: bulb_ramp_switch
- topic: "${mqtt_local_command_topic}/light/set"
payload: "${mqtt_local_device_command_OFF}"
then:
- switch.turn_off: bulb_ramp_switch
##########################################################################################
# SWITCH
##########################################################################################
switch:
- platform: template
id: bulb_ramp_switch
name: "${friendly_name} Fade Up-Down"
icon: mdi:lightbulb-on-outline
lambda: |-
return id(ramp_switch_target_on);
turn_on_action:
- lambda: |-
id(ramp_switch_target_on) = true;
- script.stop: ramp_off_script
- script.execute: ramp_on_script
turn_off_action:
- lambda: |-
id(ramp_switch_target_on) = false;
- script.stop: ramp_on_script
- script.execute: ramp_off_script
##########################################################################################
# BUTTON
##########################################################################################
button:
- platform: template
id: fade_up_button
name: "${friendly_name} Fade Up"
icon: mdi:arrow-up-bold
on_press:
- lambda: |-
id(ramp_switch_target_on) = true;
id(bulb_ramp_switch).publish_state(true);
- script.stop: ramp_off_script
- script.execute: ramp_on_script
- platform: template
id: fade_down_button
name: "${friendly_name} Fade Down"
icon: mdi:arrow-down-bold
on_press:
- lambda: |-
id(ramp_switch_target_on) = false;
id(bulb_ramp_switch).publish_state(false);
- script.stop: ramp_on_script
- script.execute: ramp_off_script
- platform: template
id: fade_stop_button
name: "${friendly_name} Fade Stop"
icon: mdi:pause
on_press:
- script.stop: ramp_on_script
- script.stop: ramp_off_script
- lambda: |-
id(ramping_for_off) = false;
id(is_ramping) = false;
const auto &cv = id(bulb_light).current_values;
if (cv.is_on()) {
auto call = id(bulb_light).make_call();
call.set_state(true);
call.set_brightness(cv.get_brightness());
call.set_transition_length(0);
call.perform();
}
if (cv.is_on()) {
float pct = cv.get_brightness() * 100.0f;
id(last_brightness_pct) = pct;
}
##########################################################################################
# SELECT
##########################################################################################
select:
- platform: template
id: restart_action
name: "${friendly_name} Restart Action"
icon: mdi:restart
optimistic: true
options:
- "Fade up to full"
- "Restore Brightness"
- "Remain Off"
initial_option: "Restore Brightness"
set_action:
- lambda: |-
if (x == "Fade up to full") {
id(restart_mode) = 0;
} else if (x == "Restore Brightness") {
id(restart_mode) = 1;
} else {
id(restart_mode) = 2;
}
##########################################################################################
# SENSORS
##########################################################################################
sensor:
- platform: template
id: bulb_output_pct
name: "${friendly_name} Output (%)"
unit_of_measurement: "%"
icon: mdi:percent
accuracy_decimals: 0
update_interval: 2s
lambda: |-
const auto &cv = id(bulb_light).current_values;
return cv.is_on() ? (float) (int)(cv.get_brightness() * 100.0f + 0.5f) : 0.0f;
on_value:
then:
- lambda: |-
if (!id(suppress_slider_sync)) {
float actual = x;
float minp = (float) id(min_brightness_pct);
float maxp = (float) id(max_brightness_pct);
if (maxp <= minp) maxp = minp + 1.0f;
float pos = (actual <= 0.0f) ? 0.0f : ((actual - minp) * 100.0f / (maxp - minp));
if (pos < 0.0f) pos = 0.0f;
if (pos > 100.0f) pos = 100.0f;
int pos_i = (int) floorf(pos + 0.5f);
if (pos_i != id(last_set_pos)) {
id(last_set_pos) = pos_i;
id(led_output_set_pct).publish_state(pos_i);
}
}
- platform: template
id: bulb_output_pwm_pct
name: "${friendly_name} Output PWM (%)"
unit_of_measurement: "%"
icon: mdi:square-wave
accuracy_decimals: 1
update_interval: 2s
lambda: |-
const auto &cv = id(bulb_light).current_values;
if (!cv.is_on()) return 0.0f;
const float lin = cv.get_brightness();
float pwm = powf(lin, id(led_gamma_f));
if (pwm < 0.0f) pwm = 0.0f;
if (pwm > 1.0f) pwm = 1.0f;
return pwm * 100.0f;
##########################################################################################
# OUTPUTS: 5 x PWM channels for the Athom RGBCT bulb
##########################################################################################
output:
- platform: esp8266_pwm
id: output_red
pin: ${bulb_red_pin}
frequency: ${pwm_frequency}
- platform: esp8266_pwm
id: output_green
pin: ${bulb_green_pin}
frequency: ${pwm_frequency}
- platform: esp8266_pwm
id: output_blue
pin: ${bulb_blue_pin}
frequency: ${pwm_frequency}
# White brightness (CT Brightness)
- platform: esp8266_pwm
id: output_white_brightness
pin: ${bulb_ct_brightness_pin}
frequency: ${pwm_frequency}
# Color temperature control (CT), inverted
- platform: esp8266_pwm
id: output_ct
pin: ${bulb_ct_pin}
frequency: ${pwm_frequency}
inverted: true
##########################################################################################
# LIGHT: RGBCT (correct for this bulb)
##########################################################################################
light:
- platform: rgbct
id: bulb_light
name: "${friendly_name}"
icon: mdi:lightbulb
red: output_red
green: output_green
blue: output_blue
white_brightness: output_white_brightness
color_temperature: output_ct
cold_white_color_temperature: ${bulb_cold_mireds}
warm_white_color_temperature: ${bulb_warm_mireds}
# Safety: do not allow RGB and CT whites simultaneously
color_interlock: true
# Important: keep OFF at boot to avoid instant restore flashes; boot script handles behavior.
restore_mode: ALWAYS_OFF
default_transition_length: 2s
gamma_correct: "${led_gamma}"
on_turn_on:
- mqtt.publish:
topic: "${mqtt_local_status_topic}/light/state"
payload: "${mqtt_local_device_command_ON}"
retain: true
- lambda: |-
if (!id(ramping_for_off)) {
id(ramp_switch_target_on) = true;
}
- script.stop: max_on_watchdog
- if:
condition:
lambda: 'return id(max_on_hours) > 0;'
then:
- script.execute: max_on_watchdog
on_turn_off:
- if:
condition:
lambda: 'return !id(booting);'
then:
- mqtt.publish:
topic: "${mqtt_local_status_topic}/light/state"
payload: "${mqtt_local_device_command_OFF}"
retain: true
- lambda: 'id(ramp_switch_target_on) = false;'
- script.stop: max_on_watchdog
- lambda: |-
id(last_brightness_pct) = 0.0f;
id(last_set_pos) = 0;
id(led_output_set_pct).publish_state(0);
# Cap brightness to max_brightness_pct without a visible step
on_state:
- lambda: |-
const float cap = id(max_brightness_pct) / 100.0f;
const auto &cv = id(bulb_light).current_values;
if (!cv.is_on()) return;
const float b = cv.get_brightness();
if (b <= cap + 0.001f) return;
auto call = id(bulb_light).make_call();
call.set_state(true);
call.set_brightness(cap);
call.set_transition_length(0);
call.perform();
##########################################################################################
# NUMBER
##########################################################################################
number:
- platform: template
id: cfg_ramp_up_s
name: "${friendly_name} Fade Up Time (s)"
entity_category: config
unit_of_measurement: s
icon: mdi:timer-sand
mode: slider
min_value: 0
max_value: 60
step: 1
lambda: |-
return (float) id(ramp_up_ms) / 1000.0f;
set_action:
- lambda: |-
int secs = (int) floorf(x + 0.5f);
if (secs < 0) secs = 0;
if (secs > 60) secs = 60;
id(ramp_up_ms) = secs * 1000;
id(cfg_ramp_up_s).publish_state((float) secs);
- platform: template
id: cfg_ramp_down_s
name: "${friendly_name} Fade Down Time (s)"
entity_category: config
unit_of_measurement: s
icon: mdi:timer-sand-complete
mode: slider
min_value: 0
max_value: 60
step: 1
lambda: |-
return (float) id(ramp_down_ms) / 1000.0f;
set_action:
- lambda: |-
int secs = (int) floorf(x + 0.5f);
if (secs < 0) secs = 0;
if (secs > 60) secs = 60;
id(ramp_down_ms) = secs * 1000;
id(cfg_ramp_down_s).publish_state((float) secs);
# This is the preferred everyday dimmer slider (FORCES Warm endpoint)
- platform: template
id: led_output_set_pct
name: "${friendly_name} Output Set (0-100)"
icon: mdi:tune
mode: slider
min_value: 0
max_value: 100
step: 1
lambda: |-
const auto &cv = id(bulb_light).current_values;
float actual = cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f;
float minp = (float) id(min_brightness_pct);
float maxp = (float) id(max_brightness_pct);
if (maxp <= minp) maxp = minp + 1.0f;
if (actual <= 0.0f) return 0.0f;
float pos = (actual - minp) * 100.0f / (maxp - minp);
if (pos < 0.0f) pos = 0.0f;
if (pos > 100.0f) pos = 100.0f;
return floorf(pos + 0.5f);
set_action:
- if:
condition:
lambda: 'return x <= 0.0f;'
then:
- lambda: 'id(suppress_slider_sync) = true;'
- script.stop: ramp_on_script
- script.stop: ramp_off_script
- light.turn_off:
id: bulb_light
transition_length: 200ms
- lambda: |-
id(ramp_switch_target_on) = false;
id(led_output_set_pct).publish_state(0);
id(last_brightness_pct) = 0.0f;
id(last_set_pos) = 0;
- delay: 400ms
- lambda: 'id(suppress_slider_sync) = false;'
else:
- lambda: |-
id(suppress_slider_sync) = true;
float pos = x;
if (pos < 1.0f) pos = 1.0f;
if (pos > 100.0f) pos = 100.0f;
id(led_output_set_pct).publish_state((int) floorf(pos + 0.5f));
- script.stop: ramp_off_script
- script.stop: ramp_on_script
- light.turn_on:
id: bulb_light
color_temperature: "${bulb_warm_kelvin}"
brightness: !lambda |-
float pos = id(led_output_set_pct).state;
float minp = (float) id(min_brightness_pct);
float maxp = (float) id(max_brightness_pct);
if (maxp <= minp) maxp = minp + 1.0f;
float out_pct = minp + (pos * (maxp - minp) / 100.0f);
if (out_pct > maxp) out_pct = maxp;
return out_pct / 100.0f;
transition_length: 250ms
- lambda: |-
id(ramp_switch_target_on) = true;
float pos = id(led_output_set_pct).state;
float minp = (float) id(min_brightness_pct);
float maxp = (float) id(max_brightness_pct);
if (maxp <= minp) maxp = minp + 1.0f;
float out_pct = minp + (pos * (maxp - minp) / 100.0f);
if (out_pct > maxp) out_pct = maxp;
id(last_brightness_pct) = out_pct;
- delay: 400ms
- lambda: 'id(suppress_slider_sync) = false;'
- platform: template
id: cfg_max_on_hours
name: "${friendly_name} Max On (h)"
entity_category: config
unit_of_measurement: h
icon: mdi:timer-cog
mode: slider
min_value: 0
max_value: 48
step: 1
lambda: |-
return (float) id(max_on_hours);
set_action:
- lambda: |-
int hrs = (int) x;
if (hrs < 0) hrs = 0;
if (hrs > 48) hrs = 48;
id(max_on_hours) = hrs;
id(cfg_max_on_hours).publish_state((float) hrs);
- if:
condition:
lambda: 'return id(bulb_light).current_values.is_on();'
then:
- script.stop: max_on_watchdog
- if:
condition:
lambda: 'return id(max_on_hours) > 0;'
then:
- script.execute: max_on_watchdog
##########################################################################################
# SCRIPTS
##########################################################################################
script:
- id: ramp_on_script
mode: restart
then:
- lambda: 'id(is_ramping) = true;'
- script.stop: ramp_off_script
- if:
condition:
lambda: |-
const auto &cv = id(bulb_light).current_values;
const float floor = id(min_brightness_pct) / 100.0f;
return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor);
then:
- light.turn_on:
id: bulb_light
color_temperature: "${bulb_warm_kelvin}"
brightness: !lambda 'return id(min_brightness_pct) / 100.0f;'
transition_length: 80ms
- light.turn_on:
id: bulb_light
color_temperature: "${bulb_warm_kelvin}"
brightness: !lambda 'return id(max_brightness_pct) / 100.0f;'
transition_length: !lambda |-
const auto &cv = id(bulb_light).current_values;
const float floor = id(min_brightness_pct) / 100.0f;
const float cap = id(max_brightness_pct) / 100.0f;
if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; }
float curr = cv.is_on() ? cv.get_brightness() : 0.0f;
if (curr + 0.0005f < floor) {
id(last_ramp_ms) = id(ramp_up_ms);
return (uint32_t) id(last_ramp_ms);
}
if (curr > cap) curr = cap;
if (curr < floor) curr = floor;
float frac = (cap - curr) / (cap - floor);
if (frac < 0.0f) frac = 0.0f;
if (frac > 1.0f) frac = 1.0f;
id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac);
return (uint32_t) id(last_ramp_ms);
- delay: !lambda 'return (uint32_t) id(last_ramp_ms);'
- lambda: |-
id(is_ramping) = false;
const auto &cv = id(bulb_light).current_values;
if (cv.is_on()) {
float pct = cv.get_brightness() * 100.0f;
id(last_brightness_pct) = pct;
}
- id: ramp_to_target_script
mode: restart
then:
- lambda: 'id(is_ramping) = true;'
- script.stop: ramp_off_script
- if:
condition:
lambda: |-
const auto &cv = id(bulb_light).current_values;
const float floor = id(min_brightness_pct) / 100.0f;
return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor);
then:
- light.turn_on:
id: bulb_light
color_temperature: "${bulb_warm_kelvin}"
brightness: !lambda 'return id(min_brightness_pct) / 100.0f;'
transition_length: 80ms
- light.turn_on:
id: bulb_light
color_temperature: "${bulb_warm_kelvin}"
brightness: !lambda |-
float cap_pct = id(restore_target_pct);
float maxp = (float) id(max_brightness_pct);
if (cap_pct > maxp) cap_pct = maxp;
return cap_pct / 100.0f;
transition_length: !lambda |-
const auto &cv = id(bulb_light).current_values;
const float floor = id(min_brightness_pct) / 100.0f;
float cap = id(restore_target_pct) / 100.0f;
const float maxp = id(max_brightness_pct) / 100.0f;
if (cap > maxp) cap = maxp;
if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; }
float curr = cv.is_on() ? cv.get_brightness() : 0.0f;
if (curr + 0.0005f < floor) {
id(last_ramp_ms) = id(ramp_up_ms);
return (uint32_t) id(last_ramp_ms);
}
if (curr > cap) curr = cap;
if (curr < floor) curr = floor;
float frac = (cap - curr) / (cap - floor);
if (frac < 0.0f) frac = 0.0f;
if (frac > 1.0f) frac = 1.0f;
id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac);
return (uint32_t) id(last_ramp_ms);
- delay: !lambda 'return (uint32_t) id(last_ramp_ms);'
- lambda: |-
id(is_ramping) = false;
const auto &cv = id(bulb_light).current_values;
if (cv.is_on()) {
float pct = cv.get_brightness() * 100.0f;
id(last_brightness_pct) = pct;
}
- id: ramp_off_script
mode: restart
then:
- lambda: |-
id(is_ramping) = true;
id(ramping_for_off) = true;
- script.stop: ramp_on_script
- light.turn_on:
id: bulb_light
color_temperature: "${bulb_warm_kelvin}"
brightness: !lambda 'return id(min_brightness_pct) / 100.0f;'
transition_length: !lambda |-
const auto &cv = id(bulb_light).current_values;
const float floor = id(min_brightness_pct) / 100.0f;
float curr = cv.get_brightness();
if (curr < floor) curr = floor;
const float denom = (1.0f - floor);
if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; }
float frac = (curr - floor) / denom;
if (frac < 0.0f) frac = 0.0f;
if (frac > 1.0f) frac = 1.0f;
id(last_ramp_ms) = (int) (id(ramp_down_ms) * frac);
return (uint32_t) id(last_ramp_ms);
- delay: !lambda 'return (uint32_t) id(last_ramp_ms);'
- light.turn_off:
id: bulb_light
transition_length: 150ms
- delay: 150ms
- lambda: |-
id(is_ramping) = false;
id(ramping_for_off) = false;
id(last_brightness_pct) = 0.0f;
id(last_set_pos) = 0;
id(led_output_set_pct).publish_state(0);
- id: max_on_watchdog
mode: restart
then:
- if:
condition:
lambda: 'return id(max_on_hours) > 0;'
then:
- delay: !lambda 'return (uint32_t) (id(max_on_hours) * 3600000UL);'
- if:
condition:
lambda: 'return id(bulb_light).current_values.is_on();'
then:
- lambda: |-
id(ramp_switch_target_on) = false;
id(bulb_ramp_switch).publish_state(false);
- script.stop: ramp_on_script
- script.execute: ramp_off_script
- id: deferred_restore_brightness
mode: restart
then:
- delay: 5s
- lambda: |-
float target = id(last_brightness_pct);
float minp = (float) id(min_brightness_pct);
float maxp = (float) id(max_brightness_pct);
if (target > 0.0f && target < minp) target = minp;
if (target > maxp) target = maxp;
id(restore_target_pct) = target;
- if:
condition:
lambda: 'return id(restore_target_pct) > 0.5f;'
then:
- lambda: |-
id(ramp_switch_target_on) = true;
id(suppress_slider_sync) = true;
- script.stop: ramp_off_script
- script.execute: ramp_to_target_script
- delay: !lambda 'return (uint32_t) id(last_ramp_ms);'
- lambda: 'id(suppress_slider_sync) = false;'
else:
- lambda: 'id(ramp_switch_target_on) = false;'
- light.turn_off:
id: bulb_light
transition_length: 0s
File diff suppressed because it is too large Load Diff
+595
View File
@@ -0,0 +1,595 @@
##########################################################################################
##########################################################################################
# DATA CUPBOARD FAN CONTROL
# Fan control for Data Cupboard
#
# Hardware: Yunshan 7-30V 10A Relay Board (HW-622) with ESP8266MOD
#
# V1.3 2026-02-20 Remove invalid allow_other_uses on pulse_counter pins (ESPHome 2026.2.x)
# V1.2 2026-02-20 Use common SNTP package, and publish fan fault/status via interval script
# V1.1 2026-02-20 Fix template binary_sensor update method for ESPHome 2026.2.x
# V1.0 2026-02-20 Initial Version
#
# NOTES
# - Fan tach inputs are on GPIO1/GPIO3 (UART pins). Logger UART is disabled (baud_rate: 0).
# - DHT22 data is on GPIO0 (boot-strap pin). Usually OK, but if you ever get boot issues,
# move the DHT22 to a different GPIO if your board allows.
# - Using common/sntp_common.yaml for time and diagnostic time text sensors.
##########################################################################################
##########################################################################################
##########################################################################################
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
##########################################################################################
substitutions:
# Device Naming
device_name: "esp-datacupboardfans"
friendly_name: "Data Cupboard Fans"
description_comment: "Fan control for Data Cupboard"
device_area: "Data Cupboard"
# Project Naming
project_name: "Yunshan.HW-622 ESP8266MOD Relay Board"
project_version: "v1.3"
# Passwords
api_key: !secret esp-api_key
ota_pass: !secret esp-ota_pass
static_ip_address: !secret esp-datacupboardfans_ip
mqtt_command_main_topic: !secret mqtt_command_main_topic
# MQTT Controls
mqtt_device_name: "data-cupboard-fans"
mqtt_main_topic: "${mqtt_command_main_topic}/${mqtt_device_name}"
# Device Settings
log_level: "ERROR"
update_interval: "60s"
# Names
relay_name: "Data Cupboard Fans Relay"
override_name: "Override Fans On"
door_name: "Data Cupboard Door Status"
temp_name: "Data Cupboard Temperature"
hum_name: "Data Cupboard Humidity"
fan1_rpm_name: "Data Cupboard Fan1 RPM"
fan2_rpm_name: "Data Cupboard Fan2 RPM"
fan_status_name: "Data Cupboard Fan Status"
# Fan tach assumptions (typical PC fan = 2 pulses per revolution)
fan_pulses_per_rev: "2"
# Fault thresholds
rpm_min_when_relay_off: "950"
rpm_min_when_relay_on: "2300"
# Default thermostat thresholds
default_temp_on: "35"
default_temp_off: "28"
##########################################################################################
# PACKAGES: Included Common Packages
##########################################################################################
packages:
#### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
common_wifi: !include
file: common/network_common.yaml
vars:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
#### HOME ASSISTANT API ####
common_api: !include
file: common/api_common.yaml
vars:
local_api_key: "${api_key}"
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### SNTP ####
common_sntp: !include
file: common/sntp_common.yaml
#### DIAGNOSTICS Sensors ####
diag_basic: !include common/include_basic_diag_sensors.yaml
diag_more: !include common/include_more_diag_sensors.yaml
##########################################################################################
# ESPHome
##########################################################################################
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
comment: "${description_comment}"
area: "${device_area}"
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: -100
then:
- lambda: |-
id(boot_ms) = millis();
id(relay_last_change_ms) = millis();
- delay: 2s
- script.execute: apply_fan_control
- script.execute: evaluate_fan_fault
##########################################################################################
# ESP Platform and Framework
##########################################################################################
esp8266:
board: esp12e
restore_from_flash: true
preferences:
flash_write_interval: 5min
mdns:
disabled: false
##########################################################################################
# LOGGING
##########################################################################################
logger:
level: "${log_level}"
baud_rate: 0
##########################################################################################
# GLOBALS
##########################################################################################
globals:
# Max temperature tracking
- id: g_max_temp_today
type: float
restore_value: yes
initial_value: "-1000.0"
- id: g_max_temp_yesterday
type: float
restore_value: yes
initial_value: "-1000.0"
- id: g_max_temp_month
type: float
restore_value: yes
initial_value: "-1000.0"
- id: g_max_temp_last_month
type: float
restore_value: yes
initial_value: "-1000.0"
- id: g_max_temp_year
type: float
restore_value: yes
initial_value: "-1000.0"
# Last seen date parts (for rollover)
- id: g_last_day
type: int
restore_value: yes
initial_value: "0"
- id: g_last_month
type: int
restore_value: yes
initial_value: "0"
- id: g_last_year
type: int
restore_value: yes
initial_value: "0"
# Fault evaluation grace timing
- id: boot_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: relay_last_change_ms
type: uint32_t
restore_value: no
initial_value: "0"
##########################################################################################
# INTERVAL (periodic evaluations)
# NOTE: common_sntp also defines its own interval (60s) for its fallback clock.
##########################################################################################
interval:
- interval: 5s
then:
- script.execute: evaluate_fan_fault
##########################################################################################
# SWITCHES (Relay + Override)
##########################################################################################
switch:
# Relay output (GPIO4) - powers both fans
- platform: gpio
name: "${relay_name}"
id: fan_relay
pin: GPIO4
restore_mode: RESTORE_DEFAULT_OFF
icon: "mdi:fan"
on_turn_on:
then:
- lambda: |-
id(relay_last_change_ms) = millis();
- script.execute: apply_fan_control
- script.execute: evaluate_fan_fault
on_turn_off:
then:
- lambda: |-
id(relay_last_change_ms) = millis();
- script.execute: apply_fan_control
- script.execute: evaluate_fan_fault
# Override switch (forces fans ON, bypasses thermostat logic)
- platform: template
name: "${override_name}"
id: override_fans_on
restore_mode: RESTORE_DEFAULT_OFF
optimistic: true
icon: "mdi:fan-plus"
entity_category: config
turn_on_action:
- switch.turn_on: fan_relay
- script.execute: apply_fan_control
- script.execute: evaluate_fan_fault
turn_off_action:
- script.execute: apply_fan_control
- script.execute: evaluate_fan_fault
##########################################################################################
# NUMBERS (On/Off thresholds with hysteresis)
##########################################################################################
number:
- platform: template
name: "Fans On Temperature"
id: temp_on_threshold
unit_of_measurement: "degC"
min_value: 25
max_value: 40
step: 0.5
mode: slider
restore_value: true
initial_value: ${default_temp_on}
optimistic: true
entity_category: config
icon: "mdi:thermometer-chevron-up"
set_action:
- script.execute: apply_fan_control
- platform: template
name: "Fans Off Temperature"
id: temp_off_threshold
unit_of_measurement: "degC"
min_value: 25
max_value: 40
step: 0.5
mode: slider
restore_value: true
initial_value: ${default_temp_off}
optimistic: true
entity_category: config
icon: "mdi:thermometer-chevron-down"
set_action:
- script.execute: apply_fan_control
##########################################################################################
# BINARY SENSORS (Door + Fault)
##########################################################################################
binary_sensor:
# Door status on GPIO5
- platform: gpio
name: "${door_name}"
id: door_status
device_class: door
pin:
number: GPIO5
mode: INPUT_PULLUP
inverted: false
filters:
- delayed_on: 50ms
- delayed_off: 50ms
# Fan fault boolean (published by script)
- platform: template
name: "Data Cupboard Fan Fault"
id: fan_fault
device_class: problem
##########################################################################################
# STATUS LED (GPIO2)
##########################################################################################
status_led:
pin:
number: GPIO2
inverted: true
##########################################################################################
# SENSORS (Temp/Humidity + Fan RPM + Max temp templates)
##########################################################################################
sensor:
# DHT22 on GPIO0
- platform: dht
pin: GPIO0
model: DHT22
update_interval: 30s
temperature:
name: "${temp_name}"
id: cupboard_temp
unit_of_measurement: "degC"
accuracy_decimals: 1
on_value:
then:
- script.execute: update_temp_maxima
- script.execute: apply_fan_control
humidity:
name: "${hum_name}"
id: cupboard_humidity
unit_of_measurement: "%"
accuracy_decimals: 1
# Fan1 tach pulses on GPIO1 (UART TX pin)
- platform: pulse_counter
pin:
number: GPIO1
mode: INPUT_PULLUP
inverted: false
id: fan1_rpm
name: "${fan1_rpm_name}"
unit_of_measurement: "rpm"
accuracy_decimals: 0
update_interval: 10s
count_mode:
rising_edge: DISABLE
falling_edge: INCREMENT
filters:
# pulse_counter is pulses/minute; typical PC fan tach = 2 pulses per revolution
- lambda: |-
return x / (float) ${fan_pulses_per_rev};
- clamp:
min_value: 0
max_value: 20000
# Fan2 tach pulses on GPIO3 (UART RX pin)
- platform: pulse_counter
pin:
number: GPIO3
mode: INPUT_PULLUP
inverted: false
id: fan2_rpm
name: "${fan2_rpm_name}"
unit_of_measurement: "rpm"
accuracy_decimals: 0
update_interval: 10s
count_mode:
rising_edge: DISABLE
falling_edge: INCREMENT
filters:
- lambda: |-
return x / (float) ${fan_pulses_per_rev};
- clamp:
min_value: 0
max_value: 20000
# Max temperature sensors (derived from cupboard_temp)
- platform: template
name: "Data Cupboard Max Temp Today"
id: max_temp_today
unit_of_measurement: "degC"
accuracy_decimals: 1
update_interval: 60s
lambda: |-
return id(g_max_temp_today);
- platform: template
name: "Data Cupboard Max Temp Yesterday"
id: max_temp_yesterday
unit_of_measurement: "degC"
accuracy_decimals: 1
update_interval: 60s
lambda: |-
return id(g_max_temp_yesterday);
- platform: template
name: "Data Cupboard Max Temp This Month"
id: max_temp_month
unit_of_measurement: "degC"
accuracy_decimals: 1
update_interval: 60s
lambda: |-
return id(g_max_temp_month);
- platform: template
name: "Data Cupboard Max Temp Last Month"
id: max_temp_last_month
unit_of_measurement: "degC"
accuracy_decimals: 1
update_interval: 60s
lambda: |-
return id(g_max_temp_last_month);
- platform: template
name: "Data Cupboard Max Temp This Year"
id: max_temp_year
unit_of_measurement: "degC"
accuracy_decimals: 1
update_interval: 60s
lambda: |-
return id(g_max_temp_year);
##########################################################################################
# TEXT SENSOR (Normal/Fault status) - published by script
# NOTE: common_sntp also defines its own text_sensors (time status etc).
##########################################################################################
text_sensor:
- platform: template
name: "${fan_status_name}"
id: fan_status
icon: "mdi:fan-alert"
##########################################################################################
# SCRIPTS
##########################################################################################
script:
# Apply fan control logic (override + hysteresis thresholds)
- id: apply_fan_control
mode: queued
then:
- lambda: |-
const float t = id(cupboard_temp).state;
if (isnan(t)) {
return;
}
float on_t = id(temp_on_threshold).state;
float off_t = id(temp_off_threshold).state;
// Guard against bad settings (keep at least 0.5C hysteresis)
if (off_t >= on_t) {
off_t = on_t - 0.5f;
}
if (id(override_fans_on).state) {
if (!id(fan_relay).state) {
id(fan_relay).turn_on();
}
return;
}
// Hysteresis-based control
if (!id(fan_relay).state) {
if (t >= on_t) {
id(fan_relay).turn_on();
}
} else {
if (t <= off_t) {
id(fan_relay).turn_off();
}
}
# Evaluate and publish fan fault and status
- id: evaluate_fan_fault
mode: restart
then:
- lambda: |-
const uint32_t now_ms = millis();
// Grace window after boot / relay change to allow RPM to stabilize
const bool in_grace =
(now_ms - id(boot_ms) < 20000) ||
(now_ms - id(relay_last_change_ms) < 15000);
if (in_grace) {
id(fan_fault).publish_state(false);
id(fan_status).publish_state("Normal");
return;
}
const float r1 = id(fan1_rpm).state;
const float r2 = id(fan2_rpm).state;
if (isnan(r1) || isnan(r2)) {
id(fan_fault).publish_state(true);
id(fan_status).publish_state("Fault");
return;
}
const float req = id(fan_relay).state ? ${rpm_min_when_relay_on} : ${rpm_min_when_relay_off};
// Requirement as requested:
// - Relay OFF => both fans must be > 1800 RPM
// - Relay ON => both fans must be > 3000 RPM
const bool fault = (r1 < req) || (r2 < req);
id(fan_fault).publish_state(fault);
id(fan_status).publish_state(fault ? "Fault" : "Normal");
# Update daily/monthly/yearly maxima based on current temperature and current date
- id: update_temp_maxima
mode: queued
then:
- lambda: |-
const float t = id(cupboard_temp).state;
if (isnan(t)) {
return;
}
auto now = id(sntp_time).now();
if (!now.is_valid()) {
// Time not valid yet: still track as rolling maxima
if (t > id(g_max_temp_today)) id(g_max_temp_today) = t;
if (t > id(g_max_temp_month)) id(g_max_temp_month) = t;
if (t > id(g_max_temp_year)) id(g_max_temp_year) = t;
} else {
// Initialize date tracking on first valid time
if (id(g_last_year) == 0) {
id(g_last_year) = now.year;
id(g_last_month) = now.month;
id(g_last_day) = now.day_of_month;
id(g_max_temp_today) = t;
id(g_max_temp_yesterday) = t;
id(g_max_temp_month) = t;
id(g_max_temp_last_month) = t;
id(g_max_temp_year) = t;
} else {
// Year rollover
if (now.year != id(g_last_year)) {
id(g_last_year) = now.year;
id(g_max_temp_year) = t;
}
// Month rollover
if (now.month != id(g_last_month)) {
id(g_max_temp_last_month) = id(g_max_temp_month);
id(g_max_temp_month) = t;
id(g_last_month) = now.month;
}
// Day rollover
if (now.day_of_month != id(g_last_day)) {
id(g_max_temp_yesterday) = id(g_max_temp_today);
id(g_max_temp_today) = t;
id(g_last_day) = now.day_of_month;
}
// Update running maxima
if (t > id(g_max_temp_today)) id(g_max_temp_today) = t;
if (t > id(g_max_temp_month)) id(g_max_temp_month) = t;
if (t > id(g_max_temp_year)) id(g_max_temp_year) = t;
}
}
// Publish template sensors promptly
id(max_temp_today).publish_state(id(g_max_temp_today));
id(max_temp_yesterday).publish_state(id(g_max_temp_yesterday));
id(max_temp_month).publish_state(id(g_max_temp_month));
id(max_temp_last_month).publish_state(id(g_max_temp_last_month));
id(max_temp_year).publish_state(id(g_max_temp_year));
##########################################################################################
# MQTT COMMANDS (device-specific)
##########################################################################################
mqtt:
on_message:
# Relay control
- topic: "${mqtt_main_topic}/relay/set"
payload: "ON"
then:
- switch.turn_on: fan_relay
- topic: "${mqtt_main_topic}/relay/set"
payload: "OFF"
then:
- switch.turn_off: fan_relay
# Override control
- topic: "${mqtt_main_topic}/override/set"
payload: "ON"
then:
- switch.turn_on: override_fans_on
- topic: "${mqtt_main_topic}/override/set"
payload: "OFF"
then:
- switch.turn_off: override_fans_on
+5
View File
@@ -71,6 +71,10 @@ substitutions:
ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-lounge6chdimmer_ip
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# Device Settings
#relay_icon: "mdi:lightbulb-group"
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
@@ -114,6 +118,7 @@ packages:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
+272 -31
View File
@@ -1,14 +1,22 @@
##########################################################################################
##########################################################################################
# MAIN BATHROOM FAN/HEAT COMBO SWITCH
# MAIN BATHROOM FAN/HEAT COMBO SWITCH
# V1.5 2026-02-23 MQTT numeric timer only applies if fan already ON (does not turn fan ON)
# V1.4 2026-02-23 Add mm:ss countdown text sensor (eg 22:12) + per-second countdown engine
# V1.3 2026-02-23 Add 60-min cap timer + Remaining Minutes sensor (counts down)
# V1.2 2026-02-18 Stop delayed-off countdown when Relay_1 turns OFF
# V1.1 2025-08-26 Minor Changes (MQTT)
# V1.0 2025-06-01 Initial Version
##########################################################################################
# Zemismart KS-811 Triple push button
# Zemismart KS-811 Double Push Button
# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/
#
# NOTES
# -
# - MQTT Local Usage
# Send ON to ${mqtt_command_main_topic}/${mqtt_local_device_name} and it will turn the fan ON (60 min cap)
# Send OFF to ${mqtt_command_main_topic}/${mqtt_local_device_name} and it will turn the fan OFF
# Send a value from 1 to 60 to ${mqtt_command_main_topic}/${mqtt_local_device_name}
# and it will ONLY start/restart the countdown IF the fan is already ON (in minutes)
#
##########################################################################################
##########################################################################################
@@ -22,26 +30,35 @@ substitutions:
device_name: "esp-mainbathfancombo"
friendly_name: "Main Bathroom Fan-Heat Combo Switch (2)"
description_comment: "Main Bathroom Fan/Heat Switch using a Zemismart KS-811 Double Push Button. Extract Fan (1), IR heater (2)"
device_area: "Laundry" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
device_area: "Main Bathroom" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
# Project Naming
project_name: "Zemismart Technologies.KS-811-2 (Double)" # Project Details
project_version: "v1.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
entity_prefix: "Main Bathroom" # Simple device name where we want to prefix a sensor or switch, eg "Load" Current.
project_name: "Zemismart Technologies.KS-811-2 (Double)" # Project Details
project_version: "v1.5" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
entity_prefix: "Main Bathroom" # Simple device name where we want to prefix a sensor or switch, eg "Load" Current.
# 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-mainbathfancombo_ip
mqtt_command_main_topic: !secret mqtt_command_main_topic
#mqtt_status_main_topic: !secret mqtt_status_main_topic
mqtt_status_main_topic: !secret mqtt_status_main_topic
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# Device Settings
#relay_icon: "mdi:heating-coil"
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_local_device1_name: "masterbath-fan"
mqtt_local_command1_topic: "${mqtt_command_main_topic}/${mqtt_local_device1_name}" # Topic we will use to command this locally without HA
mqtt_local_status1_topic: "${mqtt_status_main_topic}/${mqtt_local_device1_name}" # Topic we will use to view status locally without HA
# MQTT REMOTE Controls
mqtt_remote_device1_name: "masterbath-towelrail"
mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}/operation"
@@ -59,6 +76,7 @@ packages:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
@@ -72,7 +90,7 @@ packages:
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### WEB PORTAL ####
#common_webportal: !include common/webportal_common.yaml
#### SNTP (Only use if you want/need accurate timeclocks) ####
@@ -96,26 +114,30 @@ esphome:
project:
name: "${project_name}"
version: "${project_version}"
platformio_options:
build_flags:
- "-Os" # optimize for size
- "-Wl,--gc-sections" # drop unused code/data
#- "-fno-exceptions" # strip C++ exceptions
#- "-fno-rtti" # strip C++ RTTI
#platformio_options:
# build_flags:
# - "-Os" # optimize for size
# - "-Wl,--gc-sections" # drop unused code/data
# - "-fno-exceptions" # strip C++ exceptions
# - "-fno-rtti" # strip C++ RTTI
on_boot:
priority: -100
then:
# Avoid stale relay2_forced_relay1 across boots
- if:
condition:
switch.is_on: Relay_2
then:
- lambda: |-
id(relay2_forced_relay1) = !id(Relay_1).state;
- if:
condition:
switch.is_off: Relay_1
then:
- lambda: |-
id(relay2_forced_relay1) = true;
- switch.turn_on: Relay_1
else:
- lambda: |-
id(relay2_forced_relay1) = false;
##########################################################################################
# ESP Platform and Framework
@@ -123,16 +145,16 @@ esphome:
##########################################################################################
esp8266:
board: esp01_1m
early_pin_init: False # Initialise pins early to known values. Recommended false where switches are involved. Defaults to True.
early_pin_init: false # Recommended false where switches are involved.
board_flash_mode: dout # Default is dout
##########################################################################################
##########################################################################################
# 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)
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
@@ -141,11 +163,39 @@ logger:
# https://esphome.io/guides/automations.html?highlight=globals#global-variables
##########################################################################################
globals:
# Tracks if Relay_2 forced Relay_1 ON (so we can decide if Relay_1 should turn OFF when Relay_2 turns OFF)
- id: relay2_forced_relay1
type: bool
restore_value: yes
initial_value: "false"
# 60-min cap / countdown state for Relay_1 (Fan)
- id: relay1_timer_active
type: bool
restore_value: no
initial_value: "false"
- id: relay1_autooff_pending
type: bool
restore_value: no
initial_value: "false"
- id: relay1_remaining_min
type: int
restore_value: no
initial_value: "0"
# mm:ss countdown tracking
- id: relay1_end_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: relay1_remaining_sec
type: int
restore_value: no
initial_value: "0"
##########################################################################################
# STATUS LED
# https://esphome.io/components/status_led.html
@@ -157,6 +207,126 @@ status_led:
number: GPIO2
inverted: yes
##########################################################################################
# SENSORS
##########################################################################################
sensor:
- platform: template
name: "${entity_prefix} Fan Remaining Minutes"
id: relay1_remaining_minutes
unit_of_measurement: "min"
accuracy_decimals: 0
icon: mdi:timer-outline
update_interval: 1s
lambda: |-
return (float) id(relay1_remaining_min);
text_sensor:
- platform: template
name: "${entity_prefix} Fan Countdown"
id: relay1_countdown_text
icon: mdi:timer-outline
update_interval: 1s
lambda: |-
int sec = id(relay1_remaining_sec);
if (sec < 0) sec = 0;
int m = sec / 60;
int s = sec % 60;
char buf[12];
snprintf(buf, sizeof(buf), "%d:%02d", m, s);
return std::string(buf);
##########################################################################################
# MQTT COMMANDS
# This adds device-specific MQTT command triggers to the common MQTT configuration.
##########################################################################################
mqtt:
on_message:
# Fan control: ON / OFF / 1..60 minutes (timer only if fan already ON)
- topic: "${mqtt_local_command1_topic}"
then:
- lambda: |-
std::string p = x;
// Trim leading/trailing whitespace
while (!p.empty() && (p.back() == '\n' || p.back() == '\r' || p.back() == ' ' || p.back() == '\t')) p.pop_back();
size_t s = p.find_first_not_of(" \t\r\n");
if (s == std::string::npos) return;
p = p.substr(s);
// Uppercase
for (size_t i = 0; i < p.size(); i++) {
if (p[i] >= 'a' && p[i] <= 'z') p[i] = (char)(p[i] - 'a' + 'A');
}
if (p == "ON") {
// ON always turns fan ON (if needed) and starts/restarts 60 min cap timer
id(relay_1_delayed_off).execute(60, true);
return;
}
if (p == "OFF") {
if (id(Relay_1).state) id(Relay_1).turn_off();
return;
}
// Parse 1..60 minutes
char *endptr = nullptr;
long v = strtol(p.c_str(), &endptr, 10);
if (endptr != nullptr) {
while (*endptr == ' ' || *endptr == '\t' || *endptr == '\r' || *endptr == '\n') endptr++;
}
if (endptr != nullptr && *endptr == '\0' && v >= 1 && v <= 60) {
// Minor change: ONLY start/restart countdown if fan already ON (do not turn it ON)
if (id(Relay_1).state) {
id(relay_1_delayed_off).execute((int) v, false);
ESP_LOGI("mqtt", "Relay_1 timer restarted: %ld minute(s).", v);
} else {
ESP_LOGI("mqtt", "Ignoring timer value %ld because Relay_1 is OFF.", v);
}
} else {
ESP_LOGW("mqtt", "Ignoring payload '%s' on %s", p.c_str(), "${mqtt_local_command1_topic}");
}
##########################################################################################
# SCRIPTS
##########################################################################################
script:
# Starts/restarts mm:ss countdown, and optionally turns fan ON (used for MQTT ON / auto-start)
- id: relay_1_delayed_off
mode: restart
parameters:
off_delay_min: int
ensure_on: bool
then:
- lambda: |-
int m = off_delay_min;
if (m < 1) m = 1;
if (m > 60) m = 60;
uint32_t dur_ms = (uint32_t) m * 60UL * 1000UL;
id(relay1_end_ms) = millis() + dur_ms;
id(relay1_remaining_sec) = m * 60;
id(relay1_remaining_min) = m;
id(relay1_timer_active) = true;
id(relay1_autooff_pending) = false;
# Optionally ensure fan is ON
- if:
condition:
lambda: |-
return ensure_on;
then:
- if:
condition:
switch.is_off: Relay_1
then:
- switch.turn_on: Relay_1
##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
@@ -168,8 +338,8 @@ binary_sensor:
pin:
number: GPIO16
mode: INPUT
inverted: True
use_interrupt: false # GPO16 pin doesn't support interrupts so use polling. Only supresses a warning.
inverted: true
use_interrupt: false # GPIO16 pin doesn't support interrupts so use polling. Only suppresses a warning.
name: "Button 1: Extract Fan"
on_press:
- switch.toggle: "Relay_1"
@@ -185,14 +355,14 @@ binary_sensor:
- delayed_off: 20ms
on_multi_click:
# ── Single click: one quick press & release ─────────────────
# -- Single click: one quick press & release --
- timing:
- ON for at most 400ms
- OFF for at least 200ms
then:
- switch.toggle: "Relay_2"
# ── Double click: two quick presses/releases ───────────────
# -- Double click: two quick presses/releases --
- timing:
- ON for at most 400ms
- OFF for at most 400ms
@@ -214,8 +384,37 @@ switch:
name: "Relay 1: Extract Fan"
pin: GPIO13
id: Relay_1
on_turn_on:
then:
# Any direct ON of Relay_1 starts a 60 minute cap timer, EXCEPT:
# - When a timer/script has already started (prevents MQTT "5" being overwritten to 60)
# - When Relay_2 forced Relay_1 ON
- if:
condition:
lambda: |-
if (id(relay1_timer_active)) return false;
if (id(Relay_2).state && id(relay2_forced_relay1)) return false;
return true;
then:
- script.execute:
id: relay_1_delayed_off
off_delay_min: 60
ensure_on: false
on_turn_off:
then:
# Clear timer state
- lambda: |-
id(relay1_timer_active) = false;
id(relay1_remaining_min) = 0;
id(relay1_remaining_sec) = 0;
id(relay1_end_ms) = 0;
id(relay1_autooff_pending) = false;
# Stop any pending delayed-off countdown if Relay_1 is turned OFF (by any means)
- script.stop: relay_1_delayed_off
# Rule 3: If Relay_1 is turned OFF while Relay_2 is ON, turn OFF Relay_2 too
- if:
condition:
@@ -227,6 +426,7 @@ switch:
name: "Relay 2: IR Heater"
pin: GPIO12
id: Relay_2
on_turn_on:
then:
# Rule 2: Relay_2 implies Relay_1 must be ON
@@ -247,19 +447,60 @@ switch:
then:
- switch.turn_off: Relay_1
# If a fan timer finished while heater was ON, auto-off fan now (when heater turns OFF)
- if:
condition:
lambda: |-
return id(relay1_autooff_pending);
then:
- lambda: |-
id(relay1_autooff_pending) = false;
- if:
condition:
switch.is_on: Relay_1
then:
- switch.turn_off: Relay_1
##########################################################################################
# INTERVAL COMPONENT
# https://esphome.io/components/interval/
##########################################################################################
# An assurance that if Relay_2 (Heater) is ever on, check that Relay_1 (Fan) is on also
# Assurance: if Relay_2 (Heater) is ever on, check that Relay_1 (Fan) is on also
##########################################################################################
interval:
- interval: 1s
then:
# Countdown engine (mm:ss)
- lambda: |-
if (id(relay1_timer_active) && id(Relay_1).state) {
int32_t diff = (int32_t) (id(relay1_end_ms) - millis());
if (diff <= 0) {
id(relay1_remaining_sec) = 0;
id(relay1_remaining_min) = 0;
id(relay1_timer_active) = false;
// If heater is ON, keep fan running but remember to turn it off when heater turns off
if (id(Relay_2).state) {
id(relay1_autooff_pending) = true;
} else {
id(Relay_1).turn_off();
}
} else {
// Ceil to avoid showing 0:00 early
int sec = (diff + 999) / 1000;
id(relay1_remaining_sec) = sec;
// Minutes sensor uses ceil minutes (stays 1 until it hits 0:00)
id(relay1_remaining_min) = (sec + 59) / 60;
}
}
# Safety: if Relay_2 (Heater) is ever on, check that Relay_1 (Fan) is on also
- if:
condition:
and:
- switch.is_on: Relay_2
- switch.is_off: Relay_1
then:
- switch.turn_off: Relay_2
- switch.turn_off: Relay_2
+76 -29
View File
@@ -1,6 +1,7 @@
##########################################################################################
##########################################################################################
# MAIN BATHROOM LIGHTSWITCH 2 (OBK Converted, BK7231N)
# V2.1 2026-02-23 Button 3 multi-click publishes MQTT commands to bathroom fan (1x/2x/3x)
# V2.0 2026-02-04 Reflash into new switch (old switch electrical failure, now a BK)
# V1.0 2025-11-23 Initial
##########################################################################################
@@ -8,7 +9,7 @@
# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/
#
# NOTES
# -
# -
#
##########################################################################################
##########################################################################################
@@ -22,16 +23,22 @@ substitutions:
device_name: "esp-mainbathroomlights"
friendly_name: "Main Bathroom Lightswitch (3)"
description_comment: "BK7231N - Main Bathroom Lightswitch using a Zemismart KS-811 Triple Push Button. Main Lights (1), Shower Lights (2), Cabinet Lights (3)"
device_area: "Main Bathroom" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
device_area: "Main Bathroom" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
# Project Naming
project_name: "Zemismart Technologies.KS-811 Triple BK7231N" # Project Details
project_version: "v1.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
project_name: "Zemismart Technologies.KS-811 Triple BK7231N" # Project Details
project_version: "v2.1" # 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-garageentrylights2_ip
mqtt_command_main_topic: !secret mqtt_command_main_topic
#mqtt_status_main_topic: !secret mqtt_status_main_topic
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# Device Settings
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
@@ -40,11 +47,23 @@ substitutions:
# Default relay restore mode
# Options: RESTORE_DEFAULT_OFF, RESTORE_DEFAULT_ON, ALWAYS_ON, ALWAYS_OFF
relay_restore_mode: "RESTORE_DEFAULT_OFF"
# Switch Naming
switch_1_name: "Main Lights" # Only one light (Garage Side) actually connected to this relay
switch_2_name: "Shower Lights" # Output for pario string hanging lights
switch_3_name: "Cabinet Lights" # This is virtual only, no power connected to 3rd relay
switch_1_name: "Main Lights" # Only one light (Garage Side) actually connected to this relay
switch_2_name: "Shower Lights" # Output for pario string hanging lights
switch_3_name: "Cabinet Lights" # This is virtual only, no power connected to 3rd relay
# MQTT LOCAL Controls
#mqtt_local_device_name: "masterbath-lights"
#mqtt_local_command_topic: "${mqtt_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA
#mqtt_local_status_topic: "${mqtt_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA
# MQTT REMOTE Controls
mqtt_remote_device1_name: "masterbath-fan"
mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}"
mqtt_remote_device1_command1: "10"
mqtt_remote_device1_command2: "20"
mqtt_remote_device1_command3: "30"
##########################################################################################
# PACKAGES: Included Common Packages
@@ -58,6 +77,7 @@ packages:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
@@ -71,7 +91,7 @@ packages:
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### WEB PORTAL ####
#common_webportal: !include common/webportal_common.yaml
#### SNTP (Only use if you want/need accurate timeclocks) ####
@@ -83,7 +103,7 @@ packages:
#diag_debug: !include common/include_debug_diag_sensors.yaml
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
# Package item removals
# Package item removals
wifi:
min_auth_mode: !remove # The WPA2 only works on esp32/8266
reboot_timeout: 15min # override the selection from from the common package
@@ -104,7 +124,7 @@ esphome:
project:
name: "${project_name}"
version: "${project_version}"
# on_boot:
# on_boot:
# priority: 200
# then:
# - switch.turn_on: Relay_3
@@ -118,18 +138,9 @@ bk72xx:
board: generic-bk7231n-qfn32-tuya
##########################################################################################
# ESP Platform and Framework
# https://esphome.io/components/esp32.html
##########################################################################################
#esp8266:
# board: esp01_1m
# early_pin_init: False # Initialise pins early to known values. Recommended false where switches are involved. Defaults to True.
# board_flash_mode: dout # Default is dout
##########################################################################################
# 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)
@@ -170,8 +181,8 @@ binary_sensor:
inverted: true
name: "Button 2: ${switch_2_name}"
on_press:
- delay: 50ms
- switch.toggle: Relay_2
- delay: 50ms
- switch.toggle: Relay_2
- platform: gpio
pin:
@@ -181,11 +192,48 @@ binary_sensor:
pullup: true
inverted: true
name: "Button 3: ${switch_3_name}"
# Keep existing actions for ANY press (immediate OFF behavior)
on_press:
- delay: 50ms
#- switch.turn_off: Relay_1
- switch.turn_off: Relay_2
- switch.turn_off: Relay_3
- delay: 50ms
#- switch.turn_off: Relay_1
- switch.turn_off: Relay_2
- switch.turn_off: Relay_3
# Also publish MQTT commands depending on 1x/2x/3x press count
on_multi_click:
# -- Single click: one quick press & release --
- timing:
- ON for at most 400ms
- OFF for at least 200ms
then:
- mqtt.publish:
topic: "${mqtt_remote_device1_command_topic}"
payload: "${mqtt_remote_device1_command1}"
# -- Double click: two quick presses/releases --
- timing:
- ON for at most 400ms
- OFF for at most 400ms
- ON for at most 400ms
- OFF for at least 200ms
then:
- mqtt.publish:
topic: "${mqtt_remote_device1_command_topic}"
payload: "${mqtt_remote_device1_command2}"
# -- Triple click: three quick presses/releases --
- timing:
- ON for at most 400ms
- OFF for at most 400ms
- ON for at most 400ms
- OFF for at most 400ms
- ON for at most 400ms
- OFF for at least 200ms
then:
- mqtt.publish:
topic: "${mqtt_remote_device1_command_topic}"
payload: "${mqtt_remote_device1_command3}"
##########################################################################################
# SWITCH COMPONENT
@@ -215,7 +263,6 @@ switch:
##########################################################################################
# TEMPLATE COMPONENT
#
##########################################################################################
# Virtual group switch for Home Assistant
- platform: template
+5
View File
@@ -31,6 +31,10 @@ substitutions:
ota_pass: !secret esp-maindishwasherpower_ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-maindishwasherpower_ip
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# Device Settings
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "60s" # update time for for general sensors etc
@@ -51,6 +55,7 @@ packages:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
+1 -1
View File
@@ -77,7 +77,7 @@ packages:
local_device_name: "${device_name}"
#### WEB PORTAL ####
#common_webportal: !include common/webportal_common.yaml
common_webportal: !include common/webportal_common.yaml
#### SNTP (Only use if you want/need accurate timeclocks) ####
#common_sntp: !include common/sntp_common.yaml
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -73,8 +73,8 @@ substitutions:
static_ip_address: !secret esp-poollightpower2_ip
# Device Settings
log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "10s" # update time for for general sensors etc
log_level: "DEBUG" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "60s" # update time for for general sensors etc
# Device Settings
relay_icon: "mdi:power-socket-au"
@@ -178,13 +178,13 @@ esphome:
project:
name: "${project_name}"
version: "${project_version}"
#platformio_options:
platformio_options:
# build_unflags:
# - -std=gnu++20
# - -std=gnu++2a
# build_flags:
build_flags:
# - -std=gnu++11
# - -Os
- -Os
# - -Wl,--gc-sections
# - -fno-exceptions
# - -fno-rtti
@@ -203,7 +203,7 @@ preferences:
flash_write_interval: 10min
mdns:
disabled: true # binary size saving
disabled: False # binary size saving
##########################################################################################
# ESPHome Logging Enable
@@ -16,8 +16,10 @@
##########################################################################################
substitutions:
# Device Naming
device_name: "esp-maindishwasherspower"
friendly_name: "Main Dishwashers Power"
#device_name: "esp-maindishwasherspower"
device_name: "esp-spare1-athomv1"
#friendly_name: "Main Dishwashers Power"
friendly_name: "Spare1 Athomv1"
description_comment: "Main Dishwashers Power Monitor :: Athom Smart Plug Power Monitor V1"
device_area: "Kitchen" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.