Files
zorruno-homeassistant/esphome/esp-mainbathunderfloorheat.yaml
2026-05-24 21:41:41 +12:00

886 lines
33 KiB
YAML

#:########################################################################################:#
# TITLE: MASTER BATHROOM UNDERFLOOR HEATING
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-mainbathunderfloorheat.yaml
#:########################################################################################:#
# VERSIONS:
# V1.0 2026-05-24 Initial version based on Master Bathroom Heated Towel Rail timer/boost logic
#:########################################################################################:#
# HARDWARE:
# - Controlled by a Sonoff Mini R4 Extreme
# - Small ESP32 based device with 1 relay
# - Relay output uses GPIO26 on the standard Sonoff Mini R4 Extreme
# - Blue status LED uses GPIO19
# - Local onboard button and external S1/S2 switch input are intentionally not used in this build
# - This firmware only enables/disables power to the underfloor heating controller
# - The underfloor heating thermostat, floor temperature limit, and any thermal safety cutout are external
# - Pinout/schematic reference:
# https://devices.esphome.io/devices/sonoff-minir4-extreme/
#:########################################################################################:#
# OPERATION NOTES:
# - This device controls the Master Bathroom underfloor heating controller relay
# - It allows standalone timer style operation without Home Assistant
# - TIMER mode uses a morning and evening time window, with no weekday/weekend split
# - Morning Timer Enable and Evening Timer Enable switches gate their timer windows
# - Morning and evening timer enable switches default to ON and are restored after reboot
# - Default timer values are set in substitutions, and changed values are remembered in flash
# - Default morning timer is 04:00 to 10:00 by default
# - Default evening timer is 17:00 to 23:00 by default
# - BOOST mode turns the relay ON for the configured boost duration, then returns to TIMER mode
# - Default BOOST duration is 120 minutes by default
# - On startup or reboot, the device enters BOOST mode by default
# - Turning the "Underfloor Heating Power" HA switch ON starts BOOST mode by default
# - Turning the "Underfloor Heating Power" HA switch OFF sets operation mode to OFF
# - Operation Mode can be selected from HA as OFF, ON, TIMER, or BOOST
# - ON means permanently on until changed
# - OFF means permanently off until changed
# - TIMER means follow the enabled morning/evening schedule windows
# - BOOST means on for the boost duration, then return to TIMER
# - It uses SNTP for accurate time when WiFi and networking are available
# - If no network time is available, it uses the internal fallback minute counter
# - To reset the fallback timer after a full network outage, power cycle the device at 12pm midday
# - If a network and MQTT broker are available, operation and timer settings can be changed via MQTT
# - Any new timer values set via MQTT are remembered through a reboot
# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 30 seconds
# with about 2 seconds between toggles to allow it to boot
#:########################################################################################:#
# MQTT COMMANDS:
# Use 24 hour HH:MM format for time setting. There is no weekday/weekend setting.
#
# mqtt_timer_topic/morning-on/04:00 : Morning time the device will turn on in TIMER mode
# mqtt_timer_topic/morning-off/10:00 : Morning time the device will turn off in TIMER mode
# mqtt_timer_topic/evening-on/17:00 : Evening time the device will turn on in TIMER mode
# mqtt_timer_topic/evening-off/23:00 : Evening time the device will turn off in TIMER mode
# mqtt_timer_topic/boost-time/120 : BOOST duration in minutes (1-1439)
# mqtt_timer_topic/morning-enable/ON : Enable morning timer window
# mqtt_timer_topic/morning-enable/OFF : Disable morning timer window
# mqtt_timer_topic/evening-enable/ON : Enable evening timer window
# mqtt_timer_topic/evening-enable/OFF : Disable evening timer window
# mqtt_timer_topic/operation/ON : Device permanently on
# mqtt_timer_topic/operation/OFF : Device permanently off
# mqtt_timer_topic/operation/TIMER : Device will obey enabled timer settings
# mqtt_timer_topic/operation/BOOST : Device will turn on for BOOST duration then return to TIMER
#
# operation_mode:
# 0 = OFF
# 1 = ON
# 2 = TIMER
# 3 = BOOST
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
# - Underfloor heating will continue to follow its assigned schedule
# - Schedule times, timer enables, and operation mode can still be changed via MQTT
# b) HA/MQTT OFFLINE
# - Underfloor heating will remember its previous timer settings and mode
# - If internet/WiFi is still available, SNTP time will remain accurate
# c) Entire WiFi/Network OFFLINE
# - Underfloor heating will continue with the last remembered timer settings
# - Timeclocks will drift without SNTP and can be reset by power cycling at 12pm midday
#:########################################################################################:#
#:########################################################################################:#
# SUBSTITUTIONS: 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-mainbathunderfloorheat"
friendly_name: "Master Bathroom Underfloor Heating"
description_comment: "Sonoff Mini R4 controlling ON/OFF/TIMER/BOOST for the Master Bathroom underfloor heating controller (Layout V1.1)"
device_area: "Main Bathroom" # Allows ESP device to be automatically linked to an Area in Home Assistant.
# Project Naming
project_name: "Sonoff Technologies.Sonoff Mini R4 Extreme"
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-mainbathunderfloorheat_ip
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# Device Settings
relay_icon: "mdi:heating-coil"
log_level: "ERROR" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE
update_interval: "60s" # Update time for general sensors etc
# Timer Settings
mqtt_timer_topic: "viewroad-commands/masterbath-underfloorheat"
boost_duration_default: "120" # Minutes to stay ON in BOOST mode before reverting to TIMER
morning_on_default: "240" # Default in minutes from midnight. 04:00 => 240
morning_off_default: "600" # Default in minutes from midnight. 10:00 => 600
evening_on_default: "1020" # Default in minutes from midnight. 17:00 => 1020
evening_off_default: "1380" # Default in minutes from midnight. 23:00 => 1380
#:########################################################################################:#
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
#:########################################################################################:#
packages:
#### WIFI, Network, Static/DHCP/IPV6, 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_current_ip_address: "${current_ip_address}"
local_ota_pass: "${ota_pass}"
#### 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}"
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### WEB PORTAL ####
#common_webportal: !include common/webportal_common.yaml
#### SNTP (Only use if you want/need accurate timeclocks) ####
common_sntp: !include common/sntp_common.yaml
#### 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
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
#:########################################################################################:#
# 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}"
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 600
then:
# This evaluates if the device is power cycled 4+ times within 30 seconds.
# If so, just change to ON mode permanently.
- 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 booted 4+ times within 30 seconds, force ON mode.
if (id(boot_count) >= 4) {
id(operation_mode) = 1; // ON
ESP_LOGI("power_cycle", "Detected 4 power cycles within 30s. Forcing ON mode.");
} else {
// Normal startup logic: start in BOOST mode.
id(operation_mode) = 3;
id(boost_timer) = 0;
ESP_LOGI("power_cycle", "Boot count=%d. Starting in BOOST mode.", id(boot_count));
}
- script.execute: evaluate_relay_state
#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
# https://esphome.io/components/esp32/
#:########################################################################################:#
esp32:
variant: esp32
framework:
type: arduino
preferences:
flash_write_interval: 5min
mdns:
disabled: true
#:########################################################################################:#
# LOGGING: ESPHome Logging Enable
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
level: "${log_level}" # INFO suggested for testing, ERROR for normal use
baud_rate: 0
#:########################################################################################:#
# STATUS LED COMPONENT:
# https://esphome.io/components/status_led.html
#:########################################################################################:#
status_led:
pin:
number: GPIO19
inverted: true
#:########################################################################################:#
# SWITCH COMPONENT:
# https://esphome.io/components/switch/
#:########################################################################################:#
switch:
####################################################
# Internal physical relay
# Standard Sonoff Mini R4 Extreme relay output is GPIO26.
# This is kept internal so HA does not bypass the timer/boost mode logic.
####################################################
- platform: gpio
id: relay
pin: GPIO26
restore_mode: RESTORE_DEFAULT_OFF
internal: true
####################################################
# User-facing power switch
# ON starts BOOST mode by default.
# OFF sets operation mode OFF.
####################################################
- platform: template
name: "Underfloor Heating Power"
id: underfloor_heating_power
icon: "${relay_icon}"
lambda: |-
return id(relay).state;
turn_on_action:
- lambda: |-
id(boost_timer) = 0;
id(operation_mode) = 3;
- script.execute: evaluate_relay_state
turn_off_action:
- lambda: |-
id(boost_timer) = 0;
id(operation_mode) = 0;
- script.execute: evaluate_relay_state
####################################################
# Timer Enable Switches
# These gate the schedule windows in TIMER mode only.
# Default and restored state is ON.
####################################################
- platform: template
name: "Morning Timer Enable"
id: morning_timer_enable
restore_mode: RESTORE_DEFAULT_ON
icon: "mdi:timer-outline"
optimistic: true
on_turn_on:
- script.execute: evaluate_relay_state
on_turn_off:
- script.execute: evaluate_relay_state
- platform: template
name: "Evening Timer Enable"
id: evening_timer_enable
restore_mode: RESTORE_DEFAULT_ON
icon: "mdi:timer-outline"
optimistic: true
on_turn_on:
- script.execute: evaluate_relay_state
on_turn_off:
- script.execute: evaluate_relay_state
#:########################################################################################:#
# GLOBAL VARIABLES:
# https://esphome.io/guides/automations.html#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 the power-cycle window.
- id: boot_count
type: int
restore_value: true
initial_value: "0"
# Morning On time in minutes from midnight.
- id: morning_on
type: int
restore_value: true
initial_value: "${morning_on_default}"
# Morning Off time in minutes from midnight.
- id: morning_off
type: int
restore_value: true
initial_value: "${morning_off_default}"
# Evening On time in minutes from midnight.
- id: evening_on
type: int
restore_value: true
initial_value: "${evening_on_default}"
# Evening Off time in minutes from midnight.
- id: evening_off
type: int
restore_value: true
initial_value: "${evening_off_default}"
# Boost Duration in 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"
####################################################
# 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 SENSOR COMPONENT:
# 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;
int m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2 && h >= 0 && h <= 23 && m >= 0 && m <= 59) {
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());
}
- script.execute: evaluate_relay_state
############################
# 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;
int m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2 && h >= 0 && h <= 23 && m >= 0 && m <= 59) {
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());
}
- script.execute: evaluate_relay_state
############################
# 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;
int m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2 && h >= 0 && h <= 23 && m >= 0 && m <= 59) {
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());
}
- script.execute: evaluate_relay_state
############################
# 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;
int m = 0;
if (sscanf(x.c_str(), "%2d:%2d", &h, &m) == 2 && h >= 0 && h <= 23 && m >= 0 && m <= 59) {
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());
}
- script.execute: evaluate_relay_state
############################
# Boost duration => integer minutes (1-1439)
############################
- platform: mqtt_subscribe
name: "Boost Duration Setting"
id: boost_time_topic
topic: "${mqtt_timer_topic}/boost-time"
internal: true
on_value:
then:
- lambda: |-
int v = 0;
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 min", v);
} else {
ESP_LOGW("boost_time", "Invalid boost_time '%s'", x.c_str());
}
############################
# Morning timer enable => ON/OFF
############################
- platform: mqtt_subscribe
name: "Morning Timer Enable Setting"
id: morning_timer_enable_topic
topic: "${mqtt_timer_topic}/morning-enable"
internal: true
on_value:
then:
- lambda: |-
if (x == "ON" || x == "on" || x == "1" || x == "true" || x == "TRUE" || x == "ENABLE" || x == "enable") {
id(morning_timer_enable).turn_on();
ESP_LOGI("timer", "Morning timer enabled via MQTT.");
} else if (x == "OFF" || x == "off" || x == "0" || x == "false" || x == "FALSE" || x == "DISABLE" || x == "disable") {
id(morning_timer_enable).turn_off();
ESP_LOGI("timer", "Morning timer disabled via MQTT.");
} else {
ESP_LOGW("timer", "Invalid morning-enable value: %s", x.c_str());
}
############################
# Evening timer enable => ON/OFF
############################
- platform: mqtt_subscribe
name: "Evening Timer Enable Setting"
id: evening_timer_enable_topic
topic: "${mqtt_timer_topic}/evening-enable"
internal: true
on_value:
then:
- lambda: |-
if (x == "ON" || x == "on" || x == "1" || x == "true" || x == "TRUE" || x == "ENABLE" || x == "enable") {
id(evening_timer_enable).turn_on();
ESP_LOGI("timer", "Evening timer enabled via MQTT.");
} else if (x == "OFF" || x == "off" || x == "0" || x == "false" || x == "FALSE" || x == "DISABLE" || x == "disable") {
id(evening_timer_enable).turn_off();
ESP_LOGI("timer", "Evening timer disabled via MQTT.");
} else {
ESP_LOGW("timer", "Invalid evening-enable value: %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' || c == 't') {
id(operation_mode) = 2;
} else if (c == 'O' || c == 'o') {
// second letter N/n -> ON, F/f -> OFF
if (x.size() > 1 && (x[1] == 'N' || x[1] == 'n')) {
id(operation_mode) = 1;
} else {
id(operation_mode) = 0;
}
} else if (c == 'B' || c == 'b') {
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 as text.
####################################################
- platform: template
name: "Operation Mode State"
lambda: |-
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 HH:MM.
####################################################
- platform: template
name: "Timeclock: Morning On Time"
lambda: |-
int hour = id(morning_on) / 60;
int minute = id(morning_on) % 60;
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 HH:MM.
####################################################
- platform: template
name: "Timeclock: Morning Off Time"
lambda: |-
int hour = id(morning_off) / 60;
int minute = id(morning_off) % 60;
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 HH:MM.
####################################################
- platform: template
name: "Timeclock: Evening On Time"
lambda: |-
int hour = id(evening_on) / 60;
int minute = id(evening_on) % 60;
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 HH:MM.
####################################################
- platform: template
name: "Timeclock: Evening Off Time"
lambda: |-
int hour = id(evening_off) / 60;
int minute = id(evening_off) % 60;
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 hidden value for setting operation mode via HTTP/API if needed.
- platform: template
name: "Operation Mode Number"
id: operation_mode_number
internal: true
min_value: 0
max_value: 3
step: 1
optimistic: true
set_action:
- lambda: |-
int new_mode = int(x);
if (new_mode < 0) new_mode = 0;
if (new_mode > 3) new_mode = 3;
if (new_mode == 3) id(boost_timer) = 0;
id(operation_mode) = new_mode;
- script.execute: evaluate_relay_state
#:########################################################################################:#
# BINARY SENSOR COMPONENT:
# https://esphome.io/components/binary_sensor/
#:########################################################################################:#
binary_sensor:
- platform: template
name: "Relay Status"
id: relay_status
device_class: power
lambda: |-
return id(relay).state;
#:########################################################################################:#
# SENSOR COMPONENT:
# https://esphome.io/components/sensor/index.html
#:########################################################################################:#
sensor:
- platform: template
name: "Timeclock: Boost Duration"
id: boost_duration_time
unit_of_measurement: "min"
device_class: duration
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: "min"
accuracy_decimals: 0
update_interval: "${update_interval}"
internal: true
lambda: |-
return id(current_mins);
# A value in minutes if a timer is running, showing how many minutes are left.
- platform: template
name: "Timer Minutes Remaining"
id: timer_minutes_remaining
unit_of_measurement: "min"
device_class: duration
update_interval: 5s
accuracy_decimals: 0
lambda: |-
if (!id(relay).state) {
return 0;
}
auto in_window = [](int now_mins, int start_mins, int end_mins) -> bool {
if (start_mins == end_mins) return false;
if (start_mins < end_mins) return now_mins >= start_mins && now_mins < end_mins;
return now_mins >= start_mins || now_mins < end_mins;
};
auto minutes_until = [](int now_mins, int end_mins) -> int {
int diff = end_mins - now_mins;
if (diff <= 0) diff += 1440;
return diff;
};
int rem = 0;
if (id(operation_mode) == 2) {
if (id(morning_timer_enable).state && in_window(id(current_mins), id(morning_on), id(morning_off))) {
rem = minutes_until(id(current_mins), id(morning_off));
}
if (id(evening_timer_enable).state && in_window(id(current_mins), id(evening_on), id(evening_off))) {
int evening_rem = minutes_until(id(current_mins), id(evening_off));
rem = (rem == 0) ? evening_rem : (evening_rem < rem ? evening_rem : rem);
}
} else if (id(operation_mode) == 3) {
rem = id(boost_duration) - id(boost_timer);
}
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:
- lambda: |-
id(boost_timer) = 0;
id(operation_mode) = 3;
- script.execute: evaluate_relay_state
- platform: template
name: "Default timer settings"
id: default_timer_settings_button
icon: "mdi:restore"
on_press:
- lambda: |-
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};
id(operation_mode) = 2;
id(boost_timer) = 0;
ESP_LOGI("timer", "Default timer settings applied.");
- switch.turn_on: morning_timer_enable
- switch.turn_on: evening_timer_enable
- 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"
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");
}
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 {
id(boost_timer) = 0;
id(operation_mode) = 3;
}
- script.execute: evaluate_relay_state
#:########################################################################################:#
# SCRIPT COMPONENT:
# https://esphome.io/components/script.html
#:########################################################################################:#
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();
id(underfloor_heating_power).publish_state(id(relay).state);
return;
}
// OFF is always off.
if (mode == 0) {
id(relay).turn_off();
id(underfloor_heating_power).publish_state(id(relay).state);
return;
}
// ON is always on.
if (mode == 1) {
id(relay).turn_on();
id(underfloor_heating_power).publish_state(id(relay).state);
return;
}
// TIMER follows schedule windows, gated by the enable switches.
bool should_on = false;
auto in_window = [](int now_mins, int start_mins, int end_mins) -> bool {
if (start_mins == end_mins) return false;
if (start_mins < end_mins) return now_mins >= start_mins && now_mins < end_mins;
return now_mins >= start_mins || now_mins < end_mins;
};
if (id(morning_timer_enable).state && in_window(id(current_mins), id(morning_on), id(morning_off))) {
should_on = true;
}
if (id(evening_timer_enable).state && in_window(id(current_mins), id(evening_on), id(evening_off))) {
should_on = true;
}
if (should_on) {
id(relay).turn_on();
} else {
id(relay).turn_off();
}
id(underfloor_heating_power).publish_state(id(relay).state);
#:########################################################################################:#
# INTERVAL COMPONENT:
# https://esphome.io/components/interval.html
#:########################################################################################:#
interval:
# Bumps time using SNTP when available, otherwise uses the fallback minute counter.
# Then calls the relay evaluation script.
- interval: "1min"
then:
- lambda: |-
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 (id(operation_mode) == 3) {
id(boost_timer)++;
if (id(boost_timer) >= id(boost_duration)) {
id(operation_mode) = 2;
}
}
- script.execute: evaluate_relay_state