From 3955435c9e7dc227d191a5e95d5552c3807b5aad Mon Sep 17 00:00:00 2001 From: ESPHome Device Builder Date: Fri, 12 Jun 2026 17:32:06 +1200 Subject: [PATCH] Edit esp-gasheater.yaml --- esphome/esp-gasheater.yaml | 160 ++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 45 deletions(-) diff --git a/esphome/esp-gasheater.yaml b/esphome/esp-gasheater.yaml index 2d045e7..adad201 100644 --- a/esphome/esp-gasheater.yaml +++ b/esphome/esp-gasheater.yaml @@ -6,6 +6,10 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-gasheater.yaml #:########################################################################################:# # 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 # master setpoint number with debounced MQTT publish; restored proportional # flame tuning; kept current GPIO12/13/14 relay mapping @@ -65,12 +69,19 @@ # - AUTO may still be selected manually later for testing # - Heater Operation is a high-level thermostat enable helper: # - ON = turn Test Mode OFF (if needed) and set Mode to AUTO -# - OFF = force heater Power OFF and set Mode to BYPASS (Remote Only) -# - In any mode except BYPASS (Remote Only), 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. +# - OFF = force heater Power OFF and set Mode to OFF +# - OFF mode is the normal thermostat-off / managed-standby mode: +# - The controller will not call for heat by itself. +# - 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 -# 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: # - Applies a warmup lockout # - Calibrates to level 1 using 6x DOWN @@ -120,7 +131,7 @@ substitutions: # Project Naming project_name: "Sonoff Technologies.Sonoff DEV" - project_version: "v1.11" + project_version: "v1.12" # Passwords & Secrets api_key: !secret esp-api_key @@ -421,21 +432,33 @@ binary_sensor: if (!id(test_mode_enabled).state && id(mode_select).state == "AUTO") { id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); } - - if: - condition: - lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && !id(self_power_on);' - then: - - script.execute: enforce_heater_off - - if: - condition: - lambda: 'return !id(test_mode_enabled).state && (id(mode_select).state == "AUTO") && id(power_intended_on) && (!id(self_power_on));' - then: - - lambda: |- - id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); - - delay: ${warmup_seconds}s - - script.execute: calibrate_to_min - - script.execute: auto_control_tick - - script.execute: adjust_to_desired + - choose: + # OFF mode watches for local physical heater starts. + # If someone presses the real heater ON button, the LEDs come on, this + # 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 + + # If the heater turns on externally while AUTO already intended it ON, + # re-baseline after warmup and continue normal automatic control. + - conditions: + - lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "AUTO" && id(power_intended_on) && !id(self_power_on);' + sequence: + - lambda: |- + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: auto_control_tick + - script.execute: adjust_to_desired on_release: - lambda: |- @@ -493,7 +516,8 @@ switch: # High-level thermostat enable helper. # 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 id: heater_operation_switch name: "Heater Operation" @@ -514,10 +538,9 @@ switch: turn_off_action: - lambda: |- id(power_intended_on) = false; - - switch.turn_off: power_switch - select.set: id: mode_select - option: "BYPASS (Remote Only)" + option: "OFF" - platform: template id: power_switch @@ -650,31 +673,42 @@ text_sensor: } 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 (id(powered_state).state) { char buf[48]; snprintf(buf, sizeof(buf), "Manual (Level %d) (Test)", id(current_level)); return std::string(buf); } - if (id(led_standby_red).state) return std::string("Manual (Standby) (Test)"); return std::string("Manual (Off) (Test)"); } if (id(powered_state).state) { 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); } 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 (id(led_operating_blue).state) return std::string("Remote Only (Operating)"); - if (id(led_standby_red).state) return std::string("Remote Only (Standby)"); - return std::string("Remote Only (Off)"); + if (id(led_operating_blue).state) return std::string("Bypass (Operating)"); + if (id(led_standby_red).state) return std::string("Bypass (Standby)"); + return std::string("Bypass (Remote Only)"); } if (mode == "MANUAL") { @@ -689,14 +723,14 @@ text_sensor: if (id(led_operating_blue).state) { 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); } 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"); - return std::string("Standby"); + if (id(led_standby_red).state) return std::string("Auto (Standby)"); + return std::string("Auto (Standby)"); on_value: - script.execute: publish_status @@ -728,18 +762,24 @@ select: - platform: template id: mode_select name: "Mode" - options: ["AUTO","MANUAL","BYPASS (Remote Only)"] + options: ["OFF","AUTO","MANUAL","BYPASS (Remote Only)"] optimistic: true - initial_option: "BYPASS (Remote Only)" + initial_option: "OFF" restore_value: false set_action: - lambda: |- - if (x == "BYPASS (Remote Only)") { + if (x == "OFF" || x == "BYPASS (Remote Only)") { id(power_intended_on) = false; } if (x != "AUTO") { 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 id: flame_level_select @@ -932,13 +972,15 @@ interval: then: - 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. + # 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 then: - if: 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: - script.execute: enforce_heater_off @@ -984,7 +1026,29 @@ script: 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 mode: restart then: @@ -1003,12 +1067,13 @@ script: - script.execute: recompute_powered_state - select.set: id: mode_select - option: "BYPASS (Remote Only)" + option: "OFF" - lambda: |- 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. + # OFF mode is excluded because a physical start in OFF should trigger AUTO takeover. - id: enforce_heater_off mode: restart then: @@ -1016,7 +1081,7 @@ script: - script.execute: recompute_powered_state - if: 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: - logger.log: "Heater should be OFF but LEDs still show ON. Pulsing power to force OFF." - script.execute: press_power @@ -1125,7 +1190,7 @@ script: then: - if: 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: - lambda: |- id(calibrating) = true; @@ -1245,6 +1310,11 @@ script: mode: queued then: - 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 float t = use_test_temp ? id(test_temp_c) : id(room_temp).state; const float sp = id(setpoint_c);