Edit esp-gasheater.yaml

This commit is contained in:
ESPHome Device Builder
2026-06-12 17:32:06 +12:00
parent 6e2d5bee4d
commit 3955435c9e
+108 -38
View File
@@ -6,6 +6,10 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-gasheater.yaml # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-gasheater.yaml
#:########################################################################################:# #:########################################################################################:#
# VERSIONS: # VERSIONS:
# V1.12 2026-06-12 Added managed OFF mode separate from BYPASS (Remote Only).
# OFF is now the normal startup/thermostat-off mode and will
# take over if the heater is locally started via the physical button.
# BYPASS remains a true hands-off/remote-only service mode.
# V1.11 2026-04-26 Restored Heater Operation after regression; restored fast optimistic # V1.11 2026-04-26 Restored Heater Operation after regression; restored fast optimistic
# master setpoint number with debounced MQTT publish; restored proportional # master setpoint number with debounced MQTT publish; restored proportional
# flame tuning; kept current GPIO12/13/14 relay mapping # flame tuning; kept current GPIO12/13/14 relay mapping
@@ -65,12 +69,19 @@
# - AUTO may still be selected manually later for testing # - AUTO may still be selected manually later for testing
# - Heater Operation is a high-level thermostat enable helper: # - Heater Operation is a high-level thermostat enable helper:
# - ON = turn Test Mode OFF (if needed) and set Mode to AUTO # - ON = turn Test Mode OFF (if needed) and set Mode to AUTO
# - OFF = force heater Power OFF and set Mode to BYPASS (Remote Only) # - OFF = force heater Power OFF and set Mode to OFF
# - In any mode except BYPASS (Remote Only), if the controller intends heater power OFF # - OFF mode is the normal thermostat-off / managed-standby mode:
# but either heater LED still shows the heater ON, ESPHome will pulse the heater ON/OFF # - The controller will not call for heat by itself.
# button to force it fully OFF. # - If the heater is locally started using the physical heater button, the LEDs will
# show that the heater is ON and ESPHome will switch to AUTO and take over.
# - BYPASS (Remote Only) is a true hands-off / service mode:
# - ESPHome may still observe the LEDs/status.
# - ESPHome will not enforce OFF and will not take over after a local physical start.
# - In AUTO or MANUAL, if the controller intends heater power OFF but either heater LED
# still shows the heater ON, ESPHome will pulse the heater ON/OFF button to force it
# fully OFF.
# - On every ESPHome restart (including yaml upload or power loss), the controller first # - On every ESPHome restart (including yaml upload or power loss), the controller first
# forces the heater OFF, then returns to BYPASS (Remote Only). # forces the heater OFF, then returns to OFF mode.
# - On power ON in AUTO, the controller: # - On power ON in AUTO, the controller:
# - Applies a warmup lockout # - Applies a warmup lockout
# - Calibrates to level 1 using 6x DOWN # - Calibrates to level 1 using 6x DOWN
@@ -120,7 +131,7 @@ substitutions:
# Project Naming # Project Naming
project_name: "Sonoff Technologies.Sonoff DEV" project_name: "Sonoff Technologies.Sonoff DEV"
project_version: "v1.11" project_version: "v1.12"
# Passwords & Secrets # Passwords & Secrets
api_key: !secret esp-api_key api_key: !secret esp-api_key
@@ -421,15 +432,27 @@ binary_sensor:
if (!id(test_mode_enabled).state && id(mode_select).state == "AUTO") { if (!id(test_mode_enabled).state && id(mode_select).state == "AUTO") {
id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL);
} }
- if: - choose:
condition: # OFF mode watches for local physical heater starts.
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && !id(self_power_on);' # If someone presses the real heater ON button, the LEDs come on, this
then: # powered_state turns ON, and ESPHome changes to AUTO and takes over.
- conditions:
- lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "OFF" && !id(self_power_on);'
sequence:
- script.execute: local_on_takeover
# In AUTO/MANUAL, if the controller intended the heater OFF but LEDs say ON,
# force the heater OFF. OFF is excluded because OFF performs local-start takeover.
- conditions:
- lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && id(mode_select).state != "OFF" && !id(power_intended_on) && !id(self_power_on);'
sequence:
- script.execute: enforce_heater_off - script.execute: enforce_heater_off
- if:
condition: # If the heater turns on externally while AUTO already intended it ON,
lambda: 'return !id(test_mode_enabled).state && (id(mode_select).state == "AUTO") && id(power_intended_on) && (!id(self_power_on));' # re-baseline after warmup and continue normal automatic control.
then: - conditions:
- lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "AUTO" && id(power_intended_on) && !id(self_power_on);'
sequence:
- lambda: |- - lambda: |-
id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL);
- delay: ${warmup_seconds}s - delay: ${warmup_seconds}s
@@ -493,7 +516,8 @@ switch:
# High-level thermostat enable helper. # High-level thermostat enable helper.
# ON = thermostat logic active in AUTO # ON = thermostat logic active in AUTO
# OFF = force BYPASS and turn heater power OFF # OFF = managed OFF mode. Heater is forced OFF, but local physical starts are monitored
# and will cause ESPHome to switch back to AUTO and take over.
- platform: template - platform: template
id: heater_operation_switch id: heater_operation_switch
name: "Heater Operation" name: "Heater Operation"
@@ -514,10 +538,9 @@ switch:
turn_off_action: turn_off_action:
- lambda: |- - lambda: |-
id(power_intended_on) = false; id(power_intended_on) = false;
- switch.turn_off: power_switch
- select.set: - select.set:
id: mode_select id: mode_select
option: "BYPASS (Remote Only)" option: "OFF"
- platform: template - platform: template
id: power_switch id: power_switch
@@ -650,31 +673,42 @@ text_sensor:
} }
if (use_test_temp) { if (use_test_temp) {
if (mode == "OFF") {
return std::string("Off (Test)");
}
if (mode == "BYPASS (Remote Only)") {
return std::string("Bypass (Remote Only) (Test)");
}
if (mode == "MANUAL") { if (mode == "MANUAL") {
if (id(powered_state).state) { if (id(powered_state).state) {
char buf[48]; char buf[48];
snprintf(buf, sizeof(buf), "Manual (Level %d) (Test)", id(current_level)); snprintf(buf, sizeof(buf), "Manual (Level %d) (Test)", id(current_level));
return std::string(buf); return std::string(buf);
} }
if (id(led_standby_red).state) return std::string("Manual (Standby) (Test)");
return std::string("Manual (Off) (Test)"); return std::string("Manual (Off) (Test)");
} }
if (id(powered_state).state) { if (id(powered_state).state) {
char buf[40]; char buf[40];
snprintf(buf, sizeof(buf), "Operating (Level %d) (Test)", id(current_level)); snprintf(buf, sizeof(buf), "Auto (Level %d) (Test)", id(current_level));
return std::string(buf); return std::string(buf);
} }
if (mode == "AUTO" && !isnan(effective_t) && effective_t >= at_temp_threshold) { if (mode == "AUTO" && !isnan(effective_t) && effective_t >= at_temp_threshold) {
return std::string("At Temperature (Test)"); return std::string("Auto (At Temperature) (Test)");
} }
return std::string("Off (Test)"); return std::string("Auto (Standby) (Test)");
}
if (mode == "OFF") {
return std::string("Off");
} }
if (mode == "BYPASS (Remote Only)") { if (mode == "BYPASS (Remote Only)") {
if (id(led_operating_blue).state) return std::string("Remote Only (Operating)"); if (id(led_operating_blue).state) return std::string("Bypass (Operating)");
if (id(led_standby_red).state) return std::string("Remote Only (Standby)"); if (id(led_standby_red).state) return std::string("Bypass (Standby)");
return std::string("Remote Only (Off)"); return std::string("Bypass (Remote Only)");
} }
if (mode == "MANUAL") { if (mode == "MANUAL") {
@@ -689,14 +723,14 @@ text_sensor:
if (id(led_operating_blue).state) { if (id(led_operating_blue).state) {
char buf[32]; char buf[32];
snprintf(buf, sizeof(buf), "Operating (Level %d)", id(current_level)); snprintf(buf, sizeof(buf), "Auto (Level %d)", id(current_level));
return std::string(buf); return std::string(buf);
} }
if (!isnan(effective_t) && effective_t >= at_temp_threshold) { if (!isnan(effective_t) && effective_t >= at_temp_threshold) {
return std::string("At Temperature"); return std::string("Auto (At Temperature)");
} }
if (id(led_standby_red).state) return std::string("Standby"); if (id(led_standby_red).state) return std::string("Auto (Standby)");
return std::string("Standby"); return std::string("Auto (Standby)");
on_value: on_value:
- script.execute: publish_status - script.execute: publish_status
@@ -728,18 +762,24 @@ select:
- platform: template - platform: template
id: mode_select id: mode_select
name: "Mode" name: "Mode"
options: ["AUTO","MANUAL","BYPASS (Remote Only)"] options: ["OFF","AUTO","MANUAL","BYPASS (Remote Only)"]
optimistic: true optimistic: true
initial_option: "BYPASS (Remote Only)" initial_option: "OFF"
restore_value: false restore_value: false
set_action: set_action:
- lambda: |- - lambda: |-
if (x == "BYPASS (Remote Only)") { if (x == "OFF" || x == "BYPASS (Remote Only)") {
id(power_intended_on) = false; id(power_intended_on) = false;
} }
if (x != "AUTO") { if (x != "AUTO") {
id(room_temp_lost_fault) = false; id(room_temp_lost_fault) = false;
} }
- if:
condition:
lambda: 'return x == "OFF" && !id(test_mode_enabled).state && id(powered_state).state;'
then:
- logger.log: "Mode OFF selected: heater appears ON, pulsing power to force OFF."
- script.execute: press_power
- platform: template - platform: template
id: flame_level_select id: flame_level_select
@@ -932,13 +972,15 @@ interval:
then: then:
- script.execute: auto_control_tick - script.execute: auto_control_tick
# Outside BYPASS, if the controller intends power OFF but LEDs still show ON, # Outside OFF/BYPASS, if the controller intends power OFF but LEDs still show ON,
# keep checking and force the heater OFF. # keep checking and force the heater OFF.
# OFF is excluded because it deliberately watches for local physical starts and
# switches to AUTO instead of forcing the heater back off.
- interval: ${power_off_check_interval_s}s - interval: ${power_off_check_interval_s}s
then: then:
- if: - if:
condition: condition:
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);' lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && id(mode_select).state != "OFF" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);'
then: then:
- script.execute: enforce_heater_off - script.execute: enforce_heater_off
@@ -984,7 +1026,29 @@ script:
id(powered_state).publish_state(new_state); id(powered_state).publish_state(new_state);
# Force heater OFF after an ESPHome reboot before returning to BYPASS. # OFF-mode local physical-start takeover.
# When Mode is OFF and the heater is started from its own front panel, the LED
# inputs make powered_state turn ON. This script changes to AUTO, warms up,
# re-baselines the flame level, and resumes thermostat control.
- id: local_on_takeover
mode: restart
then:
- logger.log: "OFF mode: local physical heater start detected. Switching to AUTO and taking over."
- lambda: |-
id(power_intended_on) = true;
id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL);
id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL);
- select.set:
id: mode_select
option: "AUTO"
- delay: ${warmup_seconds}s
- script.execute: calibrate_to_min
- script.execute: auto_control_tick
- script.execute: adjust_to_desired
- lambda: |-
id(self_power_on) = false;
# Force heater OFF after an ESPHome reboot before returning to OFF.
- id: boot_force_heater_off - id: boot_force_heater_off
mode: restart mode: restart
then: then:
@@ -1003,12 +1067,13 @@ script:
- script.execute: recompute_powered_state - script.execute: recompute_powered_state
- select.set: - select.set:
id: mode_select id: mode_select
option: "BYPASS (Remote Only)" option: "OFF"
- lambda: |- - lambda: |-
id(power_intended_on) = false; id(power_intended_on) = false;
# In non-BYPASS modes, if the controller intends power OFF but LEDs still show ON, # In AUTO/MANUAL, if the controller intends power OFF but LEDs still show ON,
# pulse the heater ON/OFF button to force the heater fully OFF. # pulse the heater ON/OFF button to force the heater fully OFF.
# OFF mode is excluded because a physical start in OFF should trigger AUTO takeover.
- id: enforce_heater_off - id: enforce_heater_off
mode: restart mode: restart
then: then:
@@ -1016,7 +1081,7 @@ script:
- script.execute: recompute_powered_state - script.execute: recompute_powered_state
- if: - if:
condition: condition:
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);' lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && id(mode_select).state != "OFF" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);'
then: then:
- logger.log: "Heater should be OFF but LEDs still show ON. Pulsing power to force OFF." - logger.log: "Heater should be OFF but LEDs still show ON. Pulsing power to force OFF."
- script.execute: press_power - script.execute: press_power
@@ -1125,7 +1190,7 @@ script:
then: then:
- if: - if:
condition: condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)" && id(powered_state).state;' lambda: 'return id(mode_select).state != "BYPASS (Remote Only)" && id(mode_select).state != "OFF" && id(powered_state).state;'
then: then:
- lambda: |- - lambda: |-
id(calibrating) = true; id(calibrating) = true;
@@ -1245,6 +1310,11 @@ script:
mode: queued mode: queued
then: then:
- lambda: |- - lambda: |-
if (id(mode_select).state != "AUTO") {
ESP_LOGD("gas", "AUTO: skipping control tick because Mode is not AUTO");
return;
}
const bool use_test_temp = id(test_mode_enabled).state; const bool use_test_temp = id(test_mode_enabled).state;
const float t = use_test_temp ? id(test_temp_c) : id(room_temp).state; const float t = use_test_temp ? id(test_temp_c) : id(room_temp).state;
const float sp = id(setpoint_c); const float sp = id(setpoint_c);