Multi-Layer Safety Fireplace Controller for ESPHome

The “Safe-Fire” ESPHome Configuration

Features: 45m Auto-Off, 9m/15m Cool-down Lockouts, 90m Emergency Master Trip-Wire, and Status Reporting.

This configuration provides a fail-safe control system for gas fireplaces using ESPHome. It is designed to work with the Home Assistant Generic Thermostat while adding hardware-level safety guards that work even if Home Assistant or Wi-Fi is offline.

:fire: Key Safety Features

  • 45-Minute Max Run (Hard Stop): Automatically shuts off the fireplace after 45 minutes of continuous use to prevent overheating or accidentally leaving it on.
  • Enforced Cool-Down (15 min): After a 45-minute run, the system enters a mandatory 15-minute “lockout” where ignition is blocked, allowing components to cool.
  • Short-Cycle Protection (9 min): Prevents the gas valve from rapidly clicking on/off if the thermostat fluctuates near the target temperature.
  • Emergency Trip-Wire (90 min): If the fireplace remains on for 90 minutes for any reason, the system triggers a permanent “Emergency Shutdown.” This kills the relay and requires a human to manually flip a “Master Reset” switch in the UI before it can be used again.
  • Power-Loss Safety: Defaults to ALWAYS_OFF on reboot so the fireplace never ignites unexpectedly after a power flicker.

:hammer_and_wrench: Setup Requirements

  • Hardware: ESP32 or ESP8266 and a Relay module.
  • ESPHome Components: template_switch, script (in mode: single), and globals.
  • Home Assistant: Recommended min_cycle_duration of 10 minutes and keep_alive of 17 minutes to sync with hardware timers.
# Safety Fireplace Controller for ESPHome
# Logic: 
# 1. 45-minute max run time with 15-minute cool-down.
# 2. 9-minute short-cycle protection (synced with HA 10-minute cycle).
# 3. 90-minute Emergency Master Shutdown (Requires manual reset).

globals:
  - id: lockout_active
    type: bool
    initial_value: 'false'
  - id: master_disabled
    type: bool
    restore_value: yes # Remembers trip state after power loss
    initial_value: 'false'

switch:
  - platform: gpio
    pin: GPIO12  # Change to your relay pin
    id: relay_output
    internal: true
    restore_mode: ALWAYS_OFF

  - platform: template
    name: "Fireplace Master Enable"
    id: master_enable_switch
    optimistic: true
    icon: "mdi:shield-check"
    on_turn_on:
      - globals.set: {id: master_disabled, value: 'false'}
    on_turn_off:
      - globals.set: {id: master_disabled, value: 'true'}
      - switch.turn_off: relay_output

  - platform: template
    name: "Fireplace Control"
    id: fireplace_switch
    icon: "mdi:fireplace"
    lambda: "return id(relay_output).state;"
    turn_on_action:
      - if:
          condition:
            and:
              - lambda: 'return !id(master_disabled);'
              - lambda: 'return !id(lockout_active);'
          then:
            - switch.turn_on: relay_output
            - script.execute: safety_timer
            - script.execute: emergency_watchdog
          else:
            - logger.log: "Ignition BLOCKED: Safety check failed."
    turn_off_action:
      - switch.turn_off: relay_output
      - script.stop: safety_timer
      - script.stop: emergency_watchdog
      - if: # Only trigger short-cycle lockout if we didn't hit the 45m hard-stop
          condition:
            lambda: 'return !id(lockout_active);'
          then:
            - script.execute: force_off_timer

text_sensor:
  - platform: template
    name: "Fireplace Status"
    update_interval: 5s
    lambda: |-
      if (id(master_disabled)) return {"EMERGENCY TRIP - RESET REQ"};
      if (id(relay_output).state) return {"Heating"};
      if (id(lockout_active)) return {"Safety Lockout (Cooling)"};
      return {"Ready"};

script:
  - id: safety_timer
    mode: single # Prevents HA keep-alive from resetting the 45m clock
    then:
      - delay: 45min
      - switch.turn_off: relay_output
      - globals.set: {id: lockout_active, value: 'true'}
      - delay: 15min
      - globals.set: {id: lockout_active, value: 'false'}

  - id: force_off_timer
    mode: restart
    then:
      - globals.set: {id: lockout_active, value: 'true'}
      - delay: 9min
      - globals.set: {id: lockout_active, value: 'false'}

  - id: emergency_watchdog
    mode: single
    then:
      - delay: 90min
      - switch.turn_off: relay_output
      - globals.set: {id: master_disabled, value: 'true'}
      - switch.turn_off: master_enable_switch

1 Like