Files
zorruno-homeassistant/esphome/esp-masterbathtowelrail.yaml

710 lines
25 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#############################################
#############################################
# MASTER BATHROOM HEATED TOWEL RAIL
# Controlled by a Sonoff Basic
#
# V2.2 2025-06-14 Fixes to offline time when sntp/network unavailable
# V2.1 2025-06-12 Added select and button to chose modes, added countdown & startup to boost
# V2.0 2025-06-05 YAML Tidyups
# V1.1 2025-04-12 Fixes to timers and offline modes
# V1.0 2025-02-14 Initial Version
#
# INSTRUCTIONS
# - It allows the device to work in a standalone timer style operation
# - The timer has a morning and evening time (but no weekday/weekend settings)
# - Default values are set, but changed values are remembered in flash
# - It uses SNTP for time setting (but obviously only if wifi & networking are working)
# - It will default to an internal timer if no wifi. To reset internal timer, reboot the device at 12pm (noon)
# - If on a network and there is a MQTT server, you can set the on/off times via MQTT (See below commands)
# - You can set 4 modes ON/OFF/TIMER/BOOST via MQTT. Setting BOOST gives you a oneshot operation
# - Any new timer times set via MQTT will be remembered though a reboot
# - On startup, or a reboot, the device will always turn on for the BOOST Duration (BOOST mode, default 2 hours)
# - TIMER mode will always be switched on after BOOST mode is complete
# - Home Assistant entities are set so that BOOST mode can be pressed with a button and other modes selectable with a dropdown
# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 30 seconds (with ~2 secs in between to allow it to boot)
#
# MQTT Commands
# Values will be set in place on the update_interval time, not immediately
# Use 00:00 in 24hr format for time setting. (Note there is no weekday/weekend setting)
# mqtt_timer_topic/morning-on/06:00 : Time device will go on
# mqtt_timer_topic/morning-off/08:00 : Time device will go off
# mqtt_timer_topic/evening-on/09:00 : Time device will go on
# mqtt_timer_topic/evening-off/00:00 : Time device will go off
# mqtt_timer_topic/boost-time/0000 : Time in minutes device will temporarily go on for (1-1439)
# mqtt_timer_topic/operation/ON : Device permanently on
# mqtt_timer_topic/operation/OFF : Device permanently off
# mqtt_timer_topic/operation/TIMER : Device will obey timer settings
# mqtt_timer_topic/operation/BOOST : Turn on for (boost_duration) minutes then BOOST (also on startup)
#
# operation_mode:
# 0 = OFF
# 1 = ON
# 2 = TIMER
# 3 = BOOST
#
##########################################################################################
##########################################################################################
##########################################################################################
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
##########################################################################################
substitutions:
# Device Naming
device_name: "esp-masterbathtowelrail"
friendly_name: "Master Bathroom Towelrail"
description_comment: "Sonoff Basic controlling ON/OFF/Timer for the Heated Towel Rail in the Master Bathroom"
device_area: "Main Bathroom" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
# Project Naming
project_name: "Sonoff Technologies.Sonoff Basic V1" # Project Details
project_version: "v2.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
# Passwords
api_key: !secret esp-masterbathtowelrail_api_key # unfortunately you can't use substitutions inside secrets names
ota_pass: !secret esp-masterbathtowelrail_ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-masterbathtowelrail_ip
# Device Settings
relay_icon: "mdi:heating-coil"
log_level: "ERROR" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "60s" # update time for for general sensors etc
# Timer Settings
mqtt_timer_topic: "viewroad-commands/masterbath-towelrail" # Topics you will use to change stuff
boost_duration_default: "120" # Minutes to stay ON in BOOST mode before reverting to TIMER
morning_on_default: "300" # Default in minutes from midnight. Default 05:00 => 300
morning_off_default: "420" # Default in minutes from midnight. Default 07:00 => 420
evening_on_default: "1260" # Default in minutes from midnight. Default 21:00 => 1260
evening_off_default: "1439" # Default in minutes from midnight. Default 23:59 => 1439 => 1440 is midnight
#############################################
# Included Common Packages
# https://esphome.io/components/esphome.html
#############################################
packages:
common_wifi: !include
file: common/network_common.yaml
vars:
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
common_api: !include
file: common/api_common.yaml
vars:
local_api_key: "${api_key}"
common_webportal: !include
file: common/webportal_common.yaml
common_mqtt: !include
file: common/mqtt_common.yaml
common_sntp: !include
file: common/sntp_common.yaml
common_general_sensors: !include
file: common/sensors_common.yaml
vars:
local_friendly_name: "${friendly_name}"
local_update_interval: "${update_interval}"
#############################################
# ESPHome
# https://esphome.io/components/esphome.html
#############################################
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
comment: "${description_comment}" # Appears on the esphome page in HA
area: "${device_area}"
platformio_options:
build_flags:
- "-Os" # optimize for size
- "-Wl,--gc-sections" # drop unused code/data
- "-fno-exceptions" # strip C++ exceptions
- "-fno-rtti" # strip C++ RTTI
on_boot:
priority: 900 # High priority to run after globals are initialized
then:
- lambda: |-
// 1) Figure out the current time in "seconds from midnight"
// using SNTP if available, otherwise current_mins * 60.
bool have_sntp = id(sntp_time).now().is_valid();
int current_time_s = 0;
if (have_sntp) {
auto now = id(sntp_time).now();
current_time_s = now.hour * 3600 + now.minute * 60 + now.second;
} else {
// current_mins is in minutes; convert to seconds
current_time_s = id(current_mins) * 60;
}
// 2) Compare with the last boot time
int diff = current_time_s - id(last_boot_time_s);
// If within 30 seconds, increment boot_count; otherwise reset to 1
if (diff >= 0 && diff <= 30) {
id(boot_count)++;
} else {
id(boot_count) = 1;
}
// Update stored last boot time
id(last_boot_time_s) = current_time_s;
// 3) If we've booted 4+ times in 20s => force ON mode
if (id(boot_count) >= 4) {
id(operation_mode) = 1; // ON
ESP_LOGI("power_cycle", "Detected 4 power cycles in 20s => Forcing ON mode");
} else {
// Otherwise do your normal startup logic:
id(operation_mode) = 3; // on_boot -> sets operation_mode = 3 (BOOST)
id(boost_timer) = 0; // and reset boost_timer = 0 (for time sync if no sntp)
ESP_LOGI("power_cycle", "Boot count=%d => BOOST mode", id(boot_count));
}
#############################################
# ESP Platform and Framework
# https://esphome.io/components/esp32.html
#############################################
esp8266:
board: esp01_1m # The original sonoff basic
restore_from_flash: True # restore some values on reboot
preferences:
flash_write_interval: 5min
mdns:
disabled: true
##########################################################################################
# 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
####################################################
# SWITCH COMPONENT
# https://esphome.io/components/switch/
# Relay Switch (Sonoff Basic Relay on GPIO12)
####################################################
switch:
- platform: gpio
name: "Towel Rail Power"
pin: GPIO12
id: relay
restore_mode: RESTORE_DEFAULT_OFF
icon: "${relay_icon}"
##########################################################################################
# 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: False
initial_value: "${morning_on_default}"
# Morning Off time (minutes from midnight),
- id: morning_off
type: int
restore_value: False
initial_value: "${morning_off_default}"
# Evening On time (minutes from midnight),
- id: evening_on
type: int
restore_value: False
initial_value: "${evening_on_default}"
# Evening Off time (minutes from midnight),
- id: evening_off
type: int
restore_value: False
initial_value: "${evening_off_default}"
# Boost Duration (minutes),
- id: boost_duration
type: int
restore_value: False
initial_value: "${boost_duration_default}"
####################################################
# operation_mode:
# 0 = OFF
# 1 = ON
# 2 = TIMER
# 3 = BOOST
####################################################
- id: operation_mode
type: int
restore_value: false
initial_value: "2"
####################################################
# current_mins is set if SNTP is invalid.
# We assume user powers on the device at 12:00 noon
# => 12 * 60 = 720 minutes from midnight.
# Not restored, so it resets each boot.
####################################################
- id: current_mins
type: int
restore_value: false
initial_value: "720" # 720 is 12:00 Noon
####################################################
# boost_timer: counts minutes in BOOST mode
# After 'boost_duration' minutes, revert to TIMER.
# Not restored, so each boot starts fresh at 0.
####################################################
- id: boost_timer
type: int
restore_value: false
initial_value: "0"
##########################################################################################
# Text Sensors
# https://esphome.io/components/text_sensor/index.html
##########################################################################################
text_sensor:
############################
# MQTT Subscriptions
############################
############################
# Morning On time => "HH:MM"
############################
- platform: mqtt_subscribe
name: "Morning On Time Setting"
id: morning_on_topic
topic: "${mqtt_timer_topic}/morning-on"
internal: true
on_value:
then:
- lambda: |-
int h = 0, m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2) {
id(morning_on) = h * 60 + m;
ESP_LOGI("timer","Received new Morning On: %02d:%02d", h, m);
} 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"
internal: true
on_value:
then:
- lambda: |-
int h = 0, m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2) {
id(morning_off) = h * 60 + m;
ESP_LOGI("timer","Received new Morning Off: %02d:%02d", h, m);
} 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"
internal: true
on_value:
then:
- lambda: |-
int h = 0, m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2) {
id(evening_on) = h * 60 + m;
ESP_LOGI("timer","Received new Evening On: %02d:%02d", h, m);
} 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"
internal: true
on_value:
then:
- lambda: |-
int h = 0, m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2) {
id(evening_off) = h * 60 + m;
ESP_LOGI("timer","Received new Evening Off: %02d:%02d", h, m);
} else {
ESP_LOGW("timer","Invalid Evening Off format: %s", x.c_str());
}
############################
# Boost duration => integer minutes (11439)
############################
- platform: mqtt_subscribe
name: "Boost Duration"
id: boost_time_topic
topic: "${mqtt_timer_topic}/boost-time"
internal: true
on_value:
then:
- lambda: |-
int v = 0;
// Parse as integer
if (sscanf(x.c_str(), "%d", &v) == 1 && v >= 1 && v <= 1439) {
id(boost_duration) = v;
ESP_LOGI("boost_time","Received new Boost Duration: %d mins", v);
} else {
ESP_LOGW("boost_time","Invalid boost_time '%s'", x.c_str());
}
####################################################
# Subscribe to operation mode: OFF, ON, TIMER, BOOST
####################################################
- platform: mqtt_subscribe
id: timer_operation_mode_topic
topic: "${mqtt_timer_topic}/operation"
internal: true
on_value:
then:
- lambda: |-
// Check only the first character for mode
char c = x.c_str()[0];
if (c == 'T') { // “TIMER”
id(operation_mode) = 2;
} else if (c == 'O') { // “ON” or “OFF”
// second letter N→ON, F→OFF
id(operation_mode) = (x.size() > 1 && x[1] == 'N') ? 1 : 0;
} else if (c == 'B') { // “BOOST”
id(operation_mode) = 3;
id(boost_timer) = 0;
} else {
ESP_LOGW("timer","Invalid mode: %s", x.c_str());
}
- script.execute: evaluate_relay_state
######################################################
# Expose the current operation mode (OFF, ON, TIMER, BOOST)
######################################################
- platform: template
name: "Operation Mode State"
lambda: |-
// 0=OFF, 1=ON, 2=TIMER, 3=BOOST
switch (id(operation_mode)) {
case 0: return {"OFF"};
case 1: return {"ON"};
case 2: return {"TIMER"};
case 3: return {"BOOST"};
default: return {"UNKNOWN"};
}
update_interval: 5s
######################################################
# Expose the "Morning On" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Morning On Time"
lambda: |-
int hour = id(morning_on) / 60;
int minute = id(morning_on) % 60;
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Morning Off" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Morning Off Time"
lambda: |-
int hour = id(morning_off) / 60;
int minute = id(morning_off) % 60;
// Increase buffer size to 8 just to be safe
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Evening On" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Evening On Time"
lambda: |-
int hour = id(evening_on) / 60;
int minute = id(evening_on) % 60;
// Increase buffer size to 8 just to be safe
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
######################################################
# Expose the "Evening Off" time as a text (HH:MM)
######################################################
- platform: template
name: "Timeclock: Evening Off Time"
lambda: |-
int hour = id(evening_off) / 60;
int minute = id(evening_off) % 60;
// Increase buffer size to 8 just to be safe
// Increase to 16 for safety
char buff[16];
snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute);
return { std::string(buff) };
update_interval: "${update_interval}"
##########################################################################################
# NUMBER COMPONENT
# https://esphome.io/components/number/index.html
##########################################################################################
number:
# A value for setting operation mode via HTTP
- platform: template
name: "Operation Mode Number"
id: operation_mode_number
internal: true # ← hides it from HA
min_value: 0
max_value: 3
step: 1
set_action:
- lambda: |-
// x holds the incoming value (03)
id(operation_mode) = int(x);
##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
##########################################################################################
binary_sensor:
- platform: gpio
pin:
number: GPIO3
mode: INPUT_PULLUP
inverted: true
name: "Power Button"
id: power_button
filters:
- delayed_on: 20ms
on_click:
- min_length: 20ms
max_length: 500ms
then:
- lambda: |-
if (id(relay).state) {
// Relay is ON: turn it OFF and set mode to 0 (TIMER)
id(relay).turn_off();
id(operation_mode) = 2;
} else {
// Relay is OFF: turn it ON and set mode to 3 (BOOST)
id(relay).turn_on();
id(operation_mode) = 3;
}
- platform: template
name: "Relay Status"
lambda: |-
return id(relay).state;
##########################################################################################
# Sensors
# https://esphome.io/components/text_sensor/index.html
##########################################################################################
sensor:
- platform: template
name: "Timeclock: Boost Duration"
id: boost_duration_time
unit_of_measurement: "mins"
accuracy_decimals: "0"
update_interval: "${update_interval}"
lambda: |-
return id(boost_duration);
- platform: template
name: "Mins from Midnight"
id: mins_from_midnight
unit_of_measurement: "mins"
accuracy_decimals: "0"
update_interval: "${update_interval}"
internal: True # No need to show this in Home Assistant
lambda: |-
return id(current_mins);
# A value in mins if a timer is running showing how many mins left
- platform: template
name: "Timer Minutes Remaining"
id: timer_minutes_remaining
unit_of_measurement: "Mins"
update_interval: 5s
accuracy_decimals: "0"
lambda: |-
// always zero if relay is off
if (!id(relay).state) {
return 0;
}
int rem = 0;
// only calculate for mode 2 (scheduled) or mode 3 (BOOST)
if (id(operation_mode) == 2) {
int a = id(morning_off) - id(current_mins);
int b = id(evening_off) - id(current_mins);
// if a is negative, use b; otherwise pick the smaller of a or b
rem = (a < 0) ? b : (a < b ? a : b);
}
else if (id(operation_mode) == 3) {
rem = id(boost_duration) - id(boost_timer);
}
// never return negative
return rem > 0 ? rem : 0;
#################################################################################################
# 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
#################################################################################################
# SELECT COMPONENT
# https://esphome.io/components/select/index.html
#################################################################################################
select:
- platform: template
name: "Operation Mode"
id: operation_mode_select
update_interval: 5s
options:
- "OFF"
- "ON"
- "TIMER"
- "BOOST"
# show the current mode
lambda: |-
switch (id(operation_mode)) {
case 1: return std::string("ON");
case 2: return std::string("TIMER");
case 3: return std::string("BOOST");
default: return std::string("OFF");
}
# when changed in HA, set mode & re-evaluate
set_action:
- lambda: |-
if (x == "OFF") { id(operation_mode) = 0; }
else if (x == "ON") { id(operation_mode) = 1; }
else if (x == "TIMER") { id(operation_mode) = 2; }
else { // BOOST
id(boost_timer) = 0;
id(operation_mode) = 3;
}
- script.execute: evaluate_relay_state
#################################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
#################################################################################################
# Script: evaluate and drive the relay
script:
- id: evaluate_relay_state
then:
- lambda: |-
int mode = id(operation_mode);
// BOOST just forces the relay on
if (mode == 3) {
id(relay).turn_on();
return;
}
// OFF → always off
if (mode == 0) {
id(relay).turn_off();
return;
}
// ON → always on
if (mode == 1) {
id(relay).turn_on();
return;
}
// TIMER → follow schedule windows
{
bool should_on = false;
if (id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off))
should_on = true;
if (id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off))
should_on = true;
if (should_on) id(relay).turn_on();
else id(relay).turn_off();
}
#################################################################################################
# INTERVAL COMPONENT
# https://esphome.io/components/interval.html
#################################################################################################
# Interval: bumps time (even if no SNTP), then calls the script to evaluate relay state
interval:
- interval: "1min"
then:
- lambda: |-
// — update current_mins via SNTP or fallback
if (!id(sntp_time).now().is_valid()) {
id(current_mins)++;
if (id(current_mins) >= 1440) id(current_mins) = 0;
} else {
auto now = id(sntp_time).now();
id(current_mins) = now.hour * 60 + now.minute;
}
// — if in BOOST, advance boost_timer and expire when done
if (id(operation_mode) == 3) {
id(boost_timer)++;
if (id(boost_timer) >= id(boost_duration)) {
id(operation_mode) = 2;
//id(mqtt_client).publish("${mqtt_timer_topic}/operation", "TIMER");
}
}
- script.execute: evaluate_relay_state