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
@@ -1 +1 @@
{"pid": 67, "version": 1, "ha_version": "2026.2.2", "start_ts": 1771127640.2174048} {"pid": 68, "version": 1, "ha_version": "2026.2.3", "start_ts": 1771822737.408799}
+1
View File
@@ -0,0 +1 @@
[]
+6 -13
View File
@@ -1,6 +1,9 @@
# Configure a default setup of Home Assistant (frontend, api, etc) # Configure a default setup of Home Assistant (frontend, api, etc)
default_config: default_config:
# Helpers (default_config no longer loads input_number)
input_number:
frontend: frontend:
themes: !include_dir_merge_named themes themes: !include_dir_merge_named themes
@@ -9,35 +12,25 @@ homeassistant:
packages: !include_dir_named packages packages: !include_dir_named packages
template: !include_dir_merge_list templates template: !include_dir_merge_list templates
#group: !include groups.yaml
group: !include_dir_merge_named group/ group: !include_dir_merge_named group/
automation: !include_dir_merge_list automations/ automation: !include_dir_merge_list automations/
automation UI: !include automations.yaml
script: !include_dir_merge_named scripts/ script: !include_dir_merge_named scripts/
#script UI: !include scripts.yaml
scene: !include scenes.yaml scene: !include scenes.yaml
#mqtt merged: !include_dir_merge_named mqtt/
#sensor merged: !include_dir_merge_list sensor/
sensor: !include_dir_merge_list sensor/ sensor: !include_dir_merge_list sensor/
# Example base configuration.yaml entry
conversation: conversation:
logger: logger:
default: info default: info
# logs:
#custom_components.hacs: debug
#custom_components.evnex: debug
#evnex: debug
zha: zha:
enable_quirks: true enable_quirks: true
custom_quirks_path: /config/custom_zha_quirks/ custom_quirks_path: /config/custom_zha_quirks/
device_config: # https://community.home-assistant.io/t/turning-a-light-entity-to-switch-entity/589599/7 device_config:
a4:c1:38:02:58:65:86:ed-1: # format: {ieee}-{endpoint_id} a4:c1:38:02:58:65:86:ed-1: # format: {ieee}-{endpoint_id}
type: switch # corrected device type type: "switch" # corrected device type
a4:c1:38:a1:db:1d:38:b7-1: # format: {ieee}-{endpoint_id} a4:c1:38:a1:db:1d:38:b7-1: # format: {ieee}-{endpoint_id}
type: switch # corrected device type type: "switch" # corrected device type
zigpy_config: zigpy_config:
ota: ota:
otau_directory: /config/zigpy_ota otau_directory: /config/zigpy_ota
+1 -1
View File
@@ -51,7 +51,7 @@ wifi:
subnet: ${static_ip_subnet} subnet: ${static_ip_subnet}
dns1: ${static_ip_dns1} dns1: ${static_ip_dns1}
dns2: ${static_ip_dns2} 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 ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode
ssid: ${local_device_name} ssid: ${local_device_name}
password: ${fallback_ap_password} password: ${fallback_ap_password}
+5
View File
@@ -34,6 +34,10 @@ substitutions:
mqtt_command_main_topic: !secret mqtt_command_main_topic 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}
# MQTT LOCAL Controls # MQTT LOCAL Controls
mqtt_local_device_name: "3dprinter-power" 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 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_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}" local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}" local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) #### #### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include 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 ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-arcademachine1_ip 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 # Device Settings
log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE 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 update_interval: "10s" # update time for for general sensors etc
@@ -49,6 +53,7 @@ packages:
local_device_name: "${device_name}" local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}" local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}" local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
################################################## ##################################################
# OPTIONAL packages (comment if you don't want them) # OPTIONAL packages (comment if you don't want them)
@@ -62,8 +67,8 @@ packages:
local_api_key: "${api_key}" local_api_key: "${api_key}"
#### WEB PORTAL #### #### WEB PORTAL ####
#common_webportal: !include common_webportal: !include
# file: common/webportal_common.yaml file: common/webportal_common.yaml
#### MQTT #### #### MQTT ####
common_mqtt: !include common_mqtt: !include
@@ -124,8 +129,8 @@ esp8266:
preferences: preferences:
flash_write_interval: 10min flash_write_interval: 10min
#mdns: mdns:
# disabled: True # binary size saving disabled: False # binary size saving
########################################################################################## ##########################################################################################
# ESPHome Logging Enable # ESPHome Logging Enable
@@ -137,33 +142,6 @@ logger:
#esp8266_store_log_strings_in_flash: false #esp8266_store_log_strings_in_flash: false
#tx_buffer_size: 64 #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 # BINARY SENSORS
# https://esphome.io/components/binary_sensor/ # 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 ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-lounge6chdimmer_ip 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 # Device Settings
#relay_icon: "mdi:lightbulb-group" #relay_icon: "mdi:lightbulb-group"
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE 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_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}" local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}" local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) #### #### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include 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.1 2025-08-26 Minor Changes (MQTT)
# V1.0 2025-06-01 Initial Version # 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/ # pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/
# #
# NOTES # 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" device_name: "esp-mainbathfancombo"
friendly_name: "Main Bathroom Fan-Heat Combo Switch (2)" 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)" 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 Naming
project_name: "Zemismart Technologies.KS-811-2 (Double)" # Project Details 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 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. entity_prefix: "Main Bathroom" # Simple device name where we want to prefix a sensor or switch, eg "Load" Current.
# Passwords # Passwords
api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names 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 ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-mainbathfancombo_ip static_ip_address: !secret esp-mainbathfancombo_ip
mqtt_command_main_topic: !secret mqtt_command_main_topic 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 # Device Settings
#relay_icon: "mdi:heating-coil" #relay_icon: "mdi:heating-coil"
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE 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 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 Controls
mqtt_remote_device1_name: "masterbath-towelrail" mqtt_remote_device1_name: "masterbath-towelrail"
mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}/operation" 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_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}" local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}" local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) #### #### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include common_api: !include
@@ -72,7 +90,7 @@ packages:
file: common/mqtt_common.yaml file: common/mqtt_common.yaml
vars: vars:
local_device_name: "${device_name}" local_device_name: "${device_name}"
#### WEB PORTAL #### #### 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) #### #### SNTP (Only use if you want/need accurate timeclocks) ####
@@ -96,26 +114,30 @@ esphome:
project: project:
name: "${project_name}" name: "${project_name}"
version: "${project_version}" version: "${project_version}"
platformio_options: #platformio_options:
build_flags: # build_flags:
- "-Os" # optimize for size # - "-Os" # optimize for size
- "-Wl,--gc-sections" # drop unused code/data # - "-Wl,--gc-sections" # drop unused code/data
#- "-fno-exceptions" # strip C++ exceptions # - "-fno-exceptions" # strip C++ exceptions
#- "-fno-rtti" # strip C++ RTTI # - "-fno-rtti" # strip C++ RTTI
on_boot: on_boot:
priority: -100 priority: -100
then: then:
# Avoid stale relay2_forced_relay1 across boots
- if: - if:
condition: condition:
switch.is_on: Relay_2 switch.is_on: Relay_2
then: then:
- lambda: |-
id(relay2_forced_relay1) = !id(Relay_1).state;
- if: - if:
condition: condition:
switch.is_off: Relay_1 switch.is_off: Relay_1
then: then:
- lambda: |-
id(relay2_forced_relay1) = true;
- switch.turn_on: Relay_1 - switch.turn_on: Relay_1
else:
- lambda: |-
id(relay2_forced_relay1) = false;
########################################################################################## ##########################################################################################
# ESP Platform and Framework # ESP Platform and Framework
@@ -123,16 +145,16 @@ esphome:
########################################################################################## ##########################################################################################
esp8266: esp8266:
board: esp01_1m 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 board_flash_mode: dout # Default is dout
########################################################################################## ##########################################################################################
# ESPHome Logging Enable # ESPHome Logging Enable
# https://esphome.io/components/logger.html # https://esphome.io/components/logger.html
########################################################################################## ##########################################################################################
logger: logger:
level: "${log_level}" #INFO Level suggested, or DEBUG for testing 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) #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 #esp8266_store_log_strings_in_flash: false
#tx_buffer_size: 64 #tx_buffer_size: 64
@@ -141,11 +163,39 @@ logger:
# https://esphome.io/guides/automations.html?highlight=globals#global-variables # https://esphome.io/guides/automations.html?highlight=globals#global-variables
########################################################################################## ##########################################################################################
globals: 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 - id: relay2_forced_relay1
type: bool type: bool
restore_value: yes restore_value: yes
initial_value: "false" 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 # STATUS LED
# https://esphome.io/components/status_led.html # https://esphome.io/components/status_led.html
@@ -157,6 +207,126 @@ status_led:
number: GPIO2 number: GPIO2
inverted: yes 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 # BINARY SENSORS
# https://esphome.io/components/binary_sensor/ # https://esphome.io/components/binary_sensor/
@@ -168,8 +338,8 @@ binary_sensor:
pin: pin:
number: GPIO16 number: GPIO16
mode: INPUT mode: INPUT
inverted: True inverted: true
use_interrupt: false # GPO16 pin doesn't support interrupts so use polling. Only supresses a warning. use_interrupt: false # GPIO16 pin doesn't support interrupts so use polling. Only suppresses a warning.
name: "Button 1: Extract Fan" name: "Button 1: Extract Fan"
on_press: on_press:
- switch.toggle: "Relay_1" - switch.toggle: "Relay_1"
@@ -185,14 +355,14 @@ binary_sensor:
- delayed_off: 20ms - delayed_off: 20ms
on_multi_click: on_multi_click:
# ── Single click: one quick press & release ───────────────── # -- Single click: one quick press & release --
- timing: - timing:
- ON for at most 400ms - ON for at most 400ms
- OFF for at least 200ms - OFF for at least 200ms
then: then:
- switch.toggle: "Relay_2" - switch.toggle: "Relay_2"
# ── Double click: two quick presses/releases ─────────────── # -- Double click: two quick presses/releases --
- timing: - timing:
- ON for at most 400ms - ON for at most 400ms
- OFF for at most 400ms - OFF for at most 400ms
@@ -214,8 +384,37 @@ switch:
name: "Relay 1: Extract Fan" name: "Relay 1: Extract Fan"
pin: GPIO13 pin: GPIO13
id: Relay_1 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: on_turn_off:
then: 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 # Rule 3: If Relay_1 is turned OFF while Relay_2 is ON, turn OFF Relay_2 too
- if: - if:
condition: condition:
@@ -227,6 +426,7 @@ switch:
name: "Relay 2: IR Heater" name: "Relay 2: IR Heater"
pin: GPIO12 pin: GPIO12
id: Relay_2 id: Relay_2
on_turn_on: on_turn_on:
then: then:
# Rule 2: Relay_2 implies Relay_1 must be ON # Rule 2: Relay_2 implies Relay_1 must be ON
@@ -247,19 +447,60 @@ switch:
then: then:
- switch.turn_off: Relay_1 - 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 # INTERVAL COMPONENT
# https://esphome.io/components/interval/ # 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:
- interval: 1s - interval: 1s
then: 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: - if:
condition: condition:
and: and:
- switch.is_on: Relay_2 - switch.is_on: Relay_2
- switch.is_off: Relay_1 - switch.is_off: Relay_1
then: 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) # 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) # V2.0 2026-02-04 Reflash into new switch (old switch electrical failure, now a BK)
# V1.0 2025-11-23 Initial # V1.0 2025-11-23 Initial
########################################################################################## ##########################################################################################
@@ -8,7 +9,7 @@
# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ # pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/
# #
# NOTES # NOTES
# - # -
# #
########################################################################################## ##########################################################################################
########################################################################################## ##########################################################################################
@@ -22,16 +23,22 @@ substitutions:
device_name: "esp-mainbathroomlights" device_name: "esp-mainbathroomlights"
friendly_name: "Main Bathroom Lightswitch (3)" 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)" 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 Naming
project_name: "Zemismart Technologies.KS-811 Triple BK7231N" # Project Details 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_version: "v2.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
# Passwords # Passwords
api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names 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 ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-garageentrylights2_ip 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 # Device Settings
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
@@ -40,11 +47,23 @@ substitutions:
# Default relay restore mode # Default relay restore mode
# Options: RESTORE_DEFAULT_OFF, RESTORE_DEFAULT_ON, ALWAYS_ON, ALWAYS_OFF # Options: RESTORE_DEFAULT_OFF, RESTORE_DEFAULT_ON, ALWAYS_ON, ALWAYS_OFF
relay_restore_mode: "RESTORE_DEFAULT_OFF" relay_restore_mode: "RESTORE_DEFAULT_OFF"
# Switch Naming # Switch Naming
switch_1_name: "Main Lights" # Only one light (Garage Side) actually connected to this 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_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_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 # PACKAGES: Included Common Packages
@@ -58,6 +77,7 @@ packages:
local_device_name: "${device_name}" local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}" local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}" local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) #### #### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include common_api: !include
@@ -71,7 +91,7 @@ packages:
file: common/mqtt_common.yaml file: common/mqtt_common.yaml
vars: vars:
local_device_name: "${device_name}" local_device_name: "${device_name}"
#### WEB PORTAL #### #### 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) #### #### SNTP (Only use if you want/need accurate timeclocks) ####
@@ -83,7 +103,7 @@ packages:
#diag_debug: !include common/include_debug_diag_sensors.yaml #diag_debug: !include common/include_debug_diag_sensors.yaml
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
# Package item removals # Package item removals
wifi: wifi:
min_auth_mode: !remove # The WPA2 only works on esp32/8266 min_auth_mode: !remove # The WPA2 only works on esp32/8266
reboot_timeout: 15min # override the selection from from the common package reboot_timeout: 15min # override the selection from from the common package
@@ -104,7 +124,7 @@ esphome:
project: project:
name: "${project_name}" name: "${project_name}"
version: "${project_version}" version: "${project_version}"
# on_boot: # on_boot:
# priority: 200 # priority: 200
# then: # then:
# - switch.turn_on: Relay_3 # - switch.turn_on: Relay_3
@@ -118,18 +138,9 @@ bk72xx:
board: generic-bk7231n-qfn32-tuya 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 # ESPHome Logging Enable
# https://esphome.io/components/logger.html # https://esphome.io/components/logger.html
########################################################################################## ##########################################################################################
logger: logger:
level: "${log_level}" #INFO Level suggested, or DEBUG for testing 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) 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 inverted: true
name: "Button 2: ${switch_2_name}" name: "Button 2: ${switch_2_name}"
on_press: on_press:
- delay: 50ms - delay: 50ms
- switch.toggle: Relay_2 - switch.toggle: Relay_2
- platform: gpio - platform: gpio
pin: pin:
@@ -181,11 +192,48 @@ binary_sensor:
pullup: true pullup: true
inverted: true inverted: true
name: "Button 3: ${switch_3_name}" name: "Button 3: ${switch_3_name}"
# Keep existing actions for ANY press (immediate OFF behavior)
on_press: on_press:
- delay: 50ms - delay: 50ms
#- switch.turn_off: Relay_1 #- switch.turn_off: Relay_1
- switch.turn_off: Relay_2 - switch.turn_off: Relay_2
- switch.turn_off: Relay_3 - 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 # SWITCH COMPONENT
@@ -215,7 +263,6 @@ switch:
########################################################################################## ##########################################################################################
# TEMPLATE COMPONENT # TEMPLATE COMPONENT
#
########################################################################################## ##########################################################################################
# Virtual group switch for Home Assistant # Virtual group switch for Home Assistant
- platform: template - 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 ota_pass: !secret esp-maindishwasherpower_ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-maindishwasherpower_ip 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 # Device Settings
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE 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 update_interval: "60s" # update time for for general sensors etc
@@ -51,6 +55,7 @@ packages:
local_device_name: "${device_name}" local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}" local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}" local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) #### #### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include common_api: !include
+1 -1
View File
@@ -77,7 +77,7 @@ packages:
local_device_name: "${device_name}" local_device_name: "${device_name}"
#### WEB PORTAL #### #### 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) #### #### SNTP (Only use if you want/need accurate timeclocks) ####
#common_sntp: !include common/sntp_common.yaml #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 static_ip_address: !secret esp-poollightpower2_ip
# Device Settings # Device Settings
log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE log_level: "DEBUG" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "10s" # update time for for general sensors etc update_interval: "60s" # update time for for general sensors etc
# Device Settings # Device Settings
relay_icon: "mdi:power-socket-au" relay_icon: "mdi:power-socket-au"
@@ -178,13 +178,13 @@ esphome:
project: project:
name: "${project_name}" name: "${project_name}"
version: "${project_version}" version: "${project_version}"
#platformio_options: platformio_options:
# build_unflags: # build_unflags:
# - -std=gnu++20 # - -std=gnu++20
# - -std=gnu++2a # - -std=gnu++2a
# build_flags: build_flags:
# - -std=gnu++11 # - -std=gnu++11
# - -Os - -Os
# - -Wl,--gc-sections # - -Wl,--gc-sections
# - -fno-exceptions # - -fno-exceptions
# - -fno-rtti # - -fno-rtti
@@ -203,7 +203,7 @@ preferences:
flash_write_interval: 10min flash_write_interval: 10min
mdns: mdns:
disabled: true # binary size saving disabled: False # binary size saving
########################################################################################## ##########################################################################################
# ESPHome Logging Enable # ESPHome Logging Enable
@@ -16,8 +16,10 @@
########################################################################################## ##########################################################################################
substitutions: substitutions:
# Device Naming # Device Naming
device_name: "esp-maindishwasherspower" #device_name: "esp-maindishwasherspower"
friendly_name: "Main Dishwashers Power" 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" 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. device_area: "Kitchen" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
@@ -1,120 +0,0 @@
# ------------------------------
# TIMER HELPER (define in YAML so it definitely exists)
# ------------------------------
timer:
master_bedroom_climate_timer:
name: Master Bedroom Climate Timer
duration: "00:00:00"
# ------------------------------
# DROPDOWN (sleep timer)
# ------------------------------
input_select:
master_bedroom_climate_timer_duration:
name: Master Bedroom Aircon Sleep
options:
- "1 hour"
- "2 hours"
- "3 hours"
- "6 hours"
- "Cancel timer"
initial: "Cancel timer"
icon: mdi:timer-outline
# ------------------------------
# LIVE REMAINING (updates immediately on selection, and every minute)
# ------------------------------
template:
- trigger:
# Update once per minute (suits hh:mm display)
- platform: time_pattern
minutes: "/1"
# Update when timer STATE changes (idle/active/paused)
- platform: state
entity_id: timer.master_bedroom_climate_timer
# Update immediately when user changes the dropdown
- platform: state
entity_id: input_select.master_bedroom_climate_timer_duration
sensor:
- name: "Master Bedroom Climate Timer Remaining"
unique_id: master_bedroom_climate_timer_remaining
icon: mdi:timer
state: >-
{% set st = states('timer.master_bedroom_climate_timer') | lower %}
{% if st == 'idle' %}
0:00
{% else %}
{% set fa = state_attr('timer.master_bedroom_climate_timer', 'finishes_at') %}
{% if fa %}
{% set end = as_datetime(fa) %}
{% set sec = (end - now()).total_seconds() | int(0) %}
{% if sec < 0 %}{% set sec = 0 %}{% endif %}
{% set hh = (sec // 3600) | int %}
{% set mm = ((sec % 3600) // 60) | int %}
{{ hh }}:{{ '%02d' | format(mm) }}
{% else %}
0:00
{% endif %}
{% endif %}
# ------------------------------
# AUTOMATION: start/cancel timer when dropdown changes
# ------------------------------
automation:
- alias: "Start or Cancel Master Bedroom Climate Timer"
mode: restart
trigger:
- platform: state
entity_id: input_select.master_bedroom_climate_timer_duration
variables:
opt: "{{ states('input_select.master_bedroom_climate_timer_duration') }}"
dur_map:
"1 hour": "01:00:00"
"2 hours": "02:00:00"
"3 hours": "03:00:00"
"6 hours": "06:00:00"
action:
- choose:
- conditions:
- condition: template
value_template: "{{ opt in dur_map }}"
sequence:
- service: timer.cancel
target:
entity_id: timer.master_bedroom_climate_timer
- delay: "00:00:01"
- service: timer.start
target:
entity_id: timer.master_bedroom_climate_timer
data:
duration: "{{ dur_map[opt] }}"
- conditions:
- condition: state
entity_id: input_select.master_bedroom_climate_timer_duration
state: "Cancel timer"
sequence:
- service: timer.cancel
target:
entity_id: timer.master_bedroom_climate_timer
- alias: "Turn Off Master Bedroom Climate on Timer Completion"
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.master_bedroom_climate_timer
action:
- service: climate.turn_off
target:
entity_id: climate.master_bedroom
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_climate_timer_duration
data:
option: "Cancel timer"
@@ -1,24 +0,0 @@
automation:
# Automation to turn on the heat pump at 9pm in the master bedroom (if switched on with the helper)
- id: master_bedroom_offpeak_dehumidify
alias: Master Bedroom Off-Peak Dehumidify
mode: single
triggers:
- trigger: time
at: "21:00:00"
conditions:
- condition: state
entity_id: input_boolean.master_bedroom_offpeak_dehumidify
state: "on"
actions:
- action: climate.set_hvac_mode
target:
entity_id: climate.master_bedroom
data:
hvac_mode: "heat_cool"
- wait_for_trigger:
- trigger: time
at: "00:00:00"
- action: climate.turn_off
target:
entity_id: climate.master_bedroom
@@ -1,85 +0,0 @@
input_select:
heat_pump_temperature_threshold:
name: Master Bedroom Night Cycle Temperature Min
options:
- "18"
- "19"
- "20"
- "21"
- "22"
- "23"
initial: "20"
icon: mdi:thermometer
input_boolean:
heat_pump_automation:
name: Enable Heat Pump Master Bedroom Night Cycling
initial: off
automation:
- alias: "Master Bedroom Heat Pump Night Cycling"
description: "Turns the climate.master_bedroom to 'cool' for 15 minutes every hour if temperature is below the selected threshold."
trigger:
- platform: time_pattern # Use time_pattern to run the automation on a recurring schedule.
minutes: "/30" # The "0" means on the hour or "/15" means every 15 minutes.
condition:
# Ensure the automation is enabled
- condition: state
entity_id: input_boolean.heat_pump_automation
state: "on"
# Check that the current temperature is above the selected threshold
- condition: template
value_template: >
{{ states('sensor.master_bedroom_environment_zth01_temperature_2') | float > states('input_select.heat_pump_temperature_threshold') | float }}
# Actions: Define the steps of the automation.
action:
# Action 1: Turn on the heat pump by setting HVAC mode to "cool".
- service: climate.set_hvac_mode
target:
entity_id: climate.master_bedroom
data:
hvac_mode: cool
# Action 2: Keep the heat pump on for x minutes.
- delay: "00:10:00"
# Action 3: Turn off the heat pump.
# Some integrations do not support hvac_mode: off via climate.set_hvac_mode,
# so we use the dedicated climate.turn_off service.
- service: climate.turn_off
target:
entity_id: climate.master_bedroom
mode: single # Ensures only one instance of this automation runs at a time.
# always switch off the automation at 5am
- alias: "Disable Heat Pump Automation at 5AM"
description: "Automatically turns off the heat pump automation (input_boolean.heat_pump_automation) at 5 AM."
trigger:
- platform: time
at: "04:59:00"
action:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.heat_pump_automation
mode: single
# ensure the heat pump is off at 5am if not already
- alias: "Ensure Heat Pump Off at 5 AM"
trigger:
- platform: time
at: "05:00:00"
action:
- service: climate.turn_off
target:
entity_id: climate.master_bedroom
mode: single
# ensure the automation is off on a HA reboot
- alias: "Ensure Heat Pump Automation is Off on Startup"
description: "Turns off the heat pump automation (input_boolean.heat_pump_automation) when Home Assistant starts."
trigger:
- platform: homeassistant
event: start
action:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.heat_pump_automation
mode: single
+10 -2
View File
@@ -35,6 +35,15 @@ sensor:
#unit_prefix: k #unit_prefix: k
round: 0 round: 0
- platform: filter
name: "32A EV Charger Power 1m Avg"
unique_id: ev_charger_32a_power_1m_avg
entity_id: sensor.main_house_3_phase_power_monitor_ev_wallcharger_power
filters:
- filter: time_simple_moving_average
window_size: "00:01"
precision: 3
input_select: input_select:
leaf_ev_charging_mode: leaf_ev_charging_mode:
icon: mdi:ev-plug-type1 icon: mdi:ev-plug-type1
@@ -65,7 +74,6 @@ automation:
template: template:
- trigger: - trigger:
# Update at least every 10 seconds (and also on HA start)
- platform: time_pattern - platform: time_pattern
seconds: "/10" seconds: "/10"
- platform: homeassistant - platform: homeassistant
@@ -75,7 +83,7 @@ template:
unique_id: ev_charger_32a_charging unique_id: ev_charger_32a_charging
icon: mdi:car-electric icon: mdi:car-electric
state: >- state: >-
{{ (states('sensor.32a_ev_wallcharger_power') | float(0)) > 0.1 }} {{ states('sensor.32a_ev_charger_power_1m_avg') | float(0) > 0.1 }}
delay_on: delay_on:
seconds: 60 seconds: 60
delay_off: delay_off:
-309
View File
@@ -1,309 +0,0 @@
##########################################################################################
# heat_pump_helpers.yaml
#
# Panasonic Comfort Cloud (panasonic_cc) helpers for Heat/Cool control + presets.
#
# Purpose:
# - Provide "optimistic" UI feedback for cloud/slow mode changes
# - Provide one-tap scripts for Off/Heat/Cool/Dehumid
# - Provide a 3-step "Profile" cycler (Normal/Boost/Quiet) using preset_mode:
# * preset_mode: boost -> Powerful ON (we label this "Boost")
# * preset_mode: eco -> Quiet ON (we label this "Quiet")
# * preset_mode: none -> Powerful OFF + Quiet OFF (we label this "Normal")
#
# Notes:
# - Your climate entity supports preset_modes: [none, eco, boost]
# - The switch.lounge_ai_eco entity appears to be read-only (cannot be controlled),
# so "Eco" and "Eco/Quiet" profiles have been removed.
# - When turning the heat pump OFF, we also force preset_mode to "none" so that
# Boost/Quiet are cleared.
##########################################################################################
input_select:
# Used by your dashboard buttons to "light up" instantly (optimistic state).
# When set to something other than "none", the UI can treat that as "pending".
master_bedroom_hvac_requested_mode:
name: Master Bedroom HVAC Requested Mode
options:
- "none"
- "off"
- "heat"
- "cool"
- "dry"
initial: "none"
# Heat pump profile selector shown on the dashboard and cycled with one tap.
master_bedroom_heat_pump_profile:
name: Master Bedroom Heat Pump Profile
options:
- Normal
- Boost
- Quiet
initial: Normal
script:
########################################################################################
# One-tap HVAC mode scripts
# These set the optimistic helper first, then call the real climate services.
#
# IMPORTANT:
# - Off also clears Boost/Quiet by forcing preset_mode to "none".
########################################################################################
master_bedroom_hvac_off:
alias: Master Bedroom HVAC - Off
mode: restart
sequence:
# Optimistic UI: show "Off" immediately
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "off"
# Turn the unit off (more reliable than set_hvac_mode off on many cloud integrations)
- service: climate.turn_off
target:
entity_id: climate.master_bedroom
# Clear Panasonic preset flags: none = Boost OFF + Quiet OFF
- service: climate.set_preset_mode
target:
entity_id: climate.master_bedroom
data:
preset_mode: "none"
master_bedroom_hvac_heat:
alias: Master Bedroom HVAC - Heat
mode: restart
sequence:
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "heat"
- service: climate.turn_on
target:
entity_id: climate.master_bedroom
- service: climate.set_hvac_mode
target:
entity_id: climate.master_bedroom
data:
hvac_mode: "heat"
master_bedroom_hvac_cool:
alias: Master Bedroom HVAC - Cool
mode: restart
sequence:
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "cool"
- service: climate.turn_on
target:
entity_id: climate.master_bedroom
- service: climate.set_hvac_mode
target:
entity_id: climate.master_bedroom
data:
hvac_mode: "cool"
master_bedroom_hvac_dry:
alias: Master Bedroom HVAC - Dehumid
mode: restart
sequence:
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "dry"
- service: climate.turn_on
target:
entity_id: climate.master_bedroom
- service: climate.set_hvac_mode
target:
entity_id: climate.master_bedroom
data:
hvac_mode: "dry"
########################################################################################
# Profile logic: Apply + Cycle
#
# Profile definitions (Panasonic CC / panasonic_cc):
# - Normal -> preset_mode none (Boost OFF, Quiet OFF)
# - Boost -> preset_mode boost (Powerful ON)
# - Quiet -> preset_mode eco (Quiet ON)
#
# The profile selector is kept in-sync with the actual preset_mode via automation.
########################################################################################
master_bedroom_heat_pump_profile_apply:
alias: Master Bedroom Heat Pump - Apply Profile
mode: restart
fields:
profile:
description: Profile name (Normal, Boost, Quiet)
example: Normal
sequence:
- variables:
p: "{{ profile | default(states('input_select.master_bedroom_heat_pump_profile')) }}"
preset_for_profile: >-
{% if p == 'Normal' %}none
{% elif p == 'Boost' %}boost
{% elif p == 'Quiet' %}eco
{% else %}none
{% endif %}
# Optional: ensure the unit is on when applying a profile.
# Comment out if you want profile selection while the unit is off.
- service: climate.turn_on
target:
entity_id: climate.master_bedroom
# Set preset explicitly every time so Boost/Quiet are mutually exclusive
- service: climate.set_preset_mode
target:
entity_id: climate.master_bedroom
data:
preset_mode: "{{ preset_for_profile }}"
master_bedroom_heat_pump_profile_next:
alias: Master Bedroom Heat Pump - Next Profile
mode: restart
sequence:
- variables:
cur: "{{ states('input_select.master_bedroom_heat_pump_profile') }}"
nxt: >-
{% if cur == 'Normal' %}Boost
{% elif cur == 'Boost' %}Quiet
{% else %}Normal
{% endif %}
# Store the new selection (UI)
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_heat_pump_profile
data:
option: "{{ nxt }}"
# Apply it to the heat pump
- service: script.turn_on
target:
entity_id: script.master_bedroom_heat_pump_profile_apply
data:
variables:
profile: "{{ nxt }}"
automation:
########################################################################################
# Clear "requested mode" once the climate entity catches up, so the UI reflects reality.
# This prevents two mode buttons appearing lit due to stale cloud attributes.
########################################################################################
- id: master_bedroom_hvac_clear_requested_mode
alias: Master Bedroom HVAC - Clear Requested Mode
mode: restart
trigger:
- platform: state
entity_id: climate.master_bedroom
- platform: state
entity_id: input_select.master_bedroom_hvac_requested_mode
to:
- "off"
- "heat"
- "cool"
- "dry"
condition:
- condition: template
value_template: >
{{ states('input_select.master_bedroom_hvac_requested_mode') != 'none' }}
action:
# Small delay to allow the cloud integration to reflect the change
- delay: "00:00:03"
- choose:
- conditions:
- condition: template
value_template: >
{% set req = states('input_select.master_bedroom_hvac_requested_mode') %}
{% set mode = states('climate.master_bedroom') %}
{% set action = (state_attr('climate.master_bedroom','hvac_action') or '') %}
{{ (req == 'off' and (mode == 'off' or action == 'off'))
or (req in ['heat','cool','dry'] and mode == req) }}
sequence:
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "none"
default:
# Failsafe: if the cloud entity is slow or ambiguous, clear after a while anyway
- delay: "00:00:25"
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "none"
########################################################################################
# Sync the Profile selector from actual device preset_mode (including changes made in
# the Panasonic app). This keeps the UI truthful.
#
# Mapping:
# - preset boost -> Boost
# - preset eco -> Quiet
# - preset none -> Normal
########################################################################################
- id: master_bedroom_heat_pump_profile_sync_from_entities
alias: Master Bedroom Heat Pump - Sync Profile From Entities
mode: restart
trigger:
- platform: state
entity_id: climate.master_bedroom
action:
- delay: "00:00:02"
- variables:
preset: "{{ state_attr('climate.master_bedroom', 'preset_mode') or 'none' }}"
new_profile: >-
{% if preset == 'boost' %}
Boost
{% elif preset == 'eco' %}
Quiet
{% else %}
Normal
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ states('input_select.master_bedroom_heat_pump_profile') != new_profile }}"
sequence:
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_heat_pump_profile
data:
option: "{{ new_profile }}"
template:
- sensor:
- name: Master Bedroom Heat Pump Profile Tile
unique_id: master_bedroom_heat_pump_profile_tile
state: "{{ states('input_select.master_bedroom_heat_pump_profile') }}"
icon: >
{% set p = states('input_select.master_bedroom_heat_pump_profile') %}
{% if p == 'Boost' %}
mdi:rocket-launch
{% elif p == 'Quiet' %}
mdi:leaf
{% else %}
mdi:gauge
{% endif %}
@@ -1,76 +0,0 @@
input_number:
master_bedroom_setpoint_ui:
name: Master Bedroom Setpoint UI
min: 10
max: 30
step: 0.5
mode: box
unit_of_measurement: "°C"
icon: mdi:thermometer
script:
master_bedroom_setpoint_sync_from_climate:
alias: Master Bedroom Setpoint Sync From Climate
mode: restart
sequence:
- variables:
sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}"
- service: input_number.set_value
target:
entity_id: input_number.master_bedroom_setpoint_ui
data:
value: "{{ sp }}"
master_bedroom_setpoint_adjust:
alias: Master Bedroom Setpoint Adjust
mode: queued
fields:
step:
description: "Step in degrees C, e.g. -0.5 or 0.5"
example: -0.5
sequence:
# ─────────────────────────────
# GUARD: only block when climate is OFF/unavailable/unknown
# (Do NOT use hvac_action == idle - Panasonic cloud often reports idle)
# ─────────────────────────────
- condition: template
value_template: >
{% set s = states('climate.master_bedroom') | lower %}
{{ s not in ['off', 'unavailable', 'unknown', 'none', ''] }}
# ─────────────────────────────
# Compute new setpoint
# Prefer UI helper if valid, otherwise use climate temp
# ─────────────────────────────
- variables:
cur_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}"
cur_cl: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}"
cur: "{{ cur_ui if cur_ui > 0 else cur_cl }}"
raw: "{{ cur + (step | float(0)) }}"
new: "{{ [30, [10, raw] | max] | min | round(1) }}"
# Update UI immediately
- service: input_number.set_value
target:
entity_id: input_number.master_bedroom_setpoint_ui
data:
value: "{{ new }}"
# Send to climate (cloud may lag)
- service: climate.set_temperature
target:
entity_id: climate.master_bedroom
data:
temperature: "{{ new }}"
automation:
- id: master_bedroom_setpoint_sync
alias: Master Bedroom Setpoint Sync
mode: restart
trigger:
- platform: state
entity_id: climate.master_bedroom
- platform: homeassistant
event: start
action:
- service: script.master_bedroom_setpoint_sync_from_climate
@@ -10,7 +10,8 @@ input_number:
max: 30 max: 30
step: 0.5 step: 0.5
mode: box mode: box
unit_of_measurement: "C" unit_of_measurement: ""
initial: 21.5
############################################################################### ###############################################################################
# AUTOMATIONS # AUTOMATIONS
@@ -22,12 +23,12 @@ automation:
mode: queued mode: queued
trigger: trigger:
- platform: state - platform: state
entity_id: sensor.lounge_inside_temperature entity_id: sensor.lounge_environment_lounge_environment_temperature
action: action:
- service: mqtt.publish - service: mqtt.publish
data: data:
topic: "hasp/openhasp_1/command/p1b1.text" topic: "hasp/openhasp_1/command/p1b1.text"
payload: "{{ states('sensor.lounge_inside_temperature')|float(0)|round(1) }} C" payload: "{{ states('sensor.lounge_environment_lounge_environment_temperature')|float(0)|round(1) }} °C"
- id: hasp_setpoint_to_btn_text - id: hasp_setpoint_to_btn_text
alias: "HASP: update setpoint (p1b2.text)" alias: "HASP: update setpoint (p1b2.text)"
@@ -39,7 +40,9 @@ automation:
- service: mqtt.publish - service: mqtt.publish
data: data:
topic: "hasp/openhasp_1/command/p1b2.text" topic: "hasp/openhasp_1/command/p1b2.text"
payload: "Set: {{ states('input_number.lounge_setpoint')|float(0)|round(1) }} C" payload: "Set: {{ states('input_number.lounge_setpoint')|float(0)|round(1) }} °C"
qos: 1
retain: true
- id: hasp_heater_status_to_btn_text - id: hasp_heater_status_to_btn_text
alias: "HASP: update heater status (p1b5.text)" alias: "HASP: update heater status (p1b5.text)"
@@ -90,18 +93,24 @@ automation:
# --- PUSH INITIAL VALUES ON HA START / PANEL ONLINE ----------------------- # --- PUSH INITIAL VALUES ON HA START / PANEL ONLINE -----------------------
- id: hasp_push_on_ha_start - id: hasp_push_on_ha_start
alias: "HASP: push texts on HA start" alias: "HASP: push texts on HA start"
mode: queued
trigger: trigger:
- platform: homeassistant - platform: homeassistant
event: start event: start
action: action:
- service: script.hasp_push_all_texts - service: script.hasp_push_all_texts
- id: hasp_push_on_plate_birth - id: hasp_push_on_panel_online
alias: "HASP: push texts on panel birth" alias: "HASP: push texts when panel goes online"
mode: queued
trigger: trigger:
- platform: mqtt - platform: mqtt
topic: "hasp/openhasp_1/state" topic: "hasp/openhasp_1/status"
condition:
- condition: template
value_template: "{{ trigger.payload == 'online' }}"
action: action:
- delay: "00:00:05"
- service: script.hasp_push_all_texts - service: script.hasp_push_all_texts
############################################################################### ###############################################################################
@@ -115,11 +124,15 @@ script:
- service: mqtt.publish - service: mqtt.publish
data: data:
topic: "hasp/openhasp_1/command/p1b1.text" topic: "hasp/openhasp_1/command/p1b1.text"
payload: "{{ states('sensor.lounge_inside_temperature')|float(0)|round(1) }} C" payload: "{{ states('sensor.lounge_environment_lounge_environment_temperature')|float(0)|round(1) }} °C"
- service: mqtt.publish - service: mqtt.publish
data: data:
topic: "hasp/openhasp_1/command/p1b2.text" topic: "hasp/openhasp_1/command/p1b2.text"
payload: "Set: {{ states('input_number.lounge_setpoint')|float(0)|round(1) }} C" payload: "Set: {{ states('input_number.lounge_setpoint')|float(0)|round(1) }} °C"
qos: 1
retain: true
- service: mqtt.publish - service: mqtt.publish
data: data:
topic: "hasp/openhasp_1/command/p1b5.text" topic: "hasp/openhasp_1/command/p1b5.text"