various tidyups and esphome adds
This commit is contained in:
+1
-1
@@ -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}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
|
|||||||
+6
-13
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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 0–1439
|
|
||||||
if (endptr == x.c_str() || *endptr != '\0' || v < 0 || v > 1439) {
|
|
||||||
ESP_LOGE("boost_time", "Invalid boost_time '%s'", x.c_str());
|
|
||||||
} else {
|
|
||||||
id(boost_duration) = static_cast<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
|
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,11 +30,11 @@ 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.
|
||||||
|
|
||||||
@@ -35,13 +43,22 @@ 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-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
|
||||||
@@ -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,7 +145,7 @@ 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
|
||||||
|
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
@@ -131,8 +153,8 @@ esp8266:
|
|||||||
# 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,15 +447,56 @@ 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:
|
||||||
|
|||||||
@@ -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
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
@@ -26,12 +27,18 @@ substitutions:
|
|||||||
|
|
||||||
# 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
|
||||||
@@ -46,6 +53,18 @@ substitutions:
|
|||||||
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
|
||||||
# https://esphome.io/components/packages.html
|
# https://esphome.io/components/packages.html
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -117,15 +137,6 @@ esphome:
|
|||||||
bk72xx:
|
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
|
||||||
@@ -181,12 +192,49 @@ 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
|
||||||
# https://esphome.io/components/switch/
|
# https://esphome.io/components/switch/
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
|
||||||
+22
-9
@@ -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"
|
||||||
Reference in New Issue
Block a user