Utility Power to Generator Automatic Switching for Predator 8750/7000 Inverter

When a friend paid $13K last year to have a 24kW Generac whole house standby generator installed, I was jealous. I had, and still have a Predator 8750/7000W inverter generator. I paid just under $1,00 for this as mine had a damaged packing box. This was connected to my house via a 50A outdoor receptacle and then into my load center via an interlock plate and 2 x 50A breakers. This worked well but my wife could never seem to master it. I resolved to try and improve things without breaking the bank. It has cost me just under $4K including the inverter generator and it works fine. I can run anything I want but not everything at once.

What I wanted was a fully automatic changeover system to generator from utility power in the event of utility failure. I also wished to have it return to utility power and stop the generator when utility power was restored. Obviously it had to be connected to HA but, I wanted it to work anyway even if HA was down. I now have this and here are all the schematics, yaml and Lovelace files to make it work. Please note that this was designed by me, for me, at my home, with my generator. I do not know if this will work with any other generator. I suspect some could possibly be modified but I have no idea how to do that so, please do not ask.

I had a licensed and bonded electrician install the Automatic Transfer Switch and he pulled a permit from the city. It has now been inspected and has passed that.

THIS PROJECT REQUIES REWIRING THE MAIN POWER FEED(s) FROM YOUR ELECTRICITY COMPANY TO YOUR HOUSE. DO NOT, REPEAT DO NOT ATTEMPT THIS WORK UNLESS YOU ARE TRAINED AND ARE COMPETENT IN THIS WORK. IT CAN BE DANGEROUS AND COULD KILL YOU. I ACCEPT ABSOLUTELY NO RESPONSIBILITY FOR ANY USE OF THIS PROJECT AND I POST IT HERE SIMPLY FOR YOUR INTEREST.

I used:

I had the initial idea and worked out the schematic. I wrote the initial yaml and it grew considerably in complexity during development. Claude (claude.ai) by Anthropic was an invaluable coding partner throughout, handling the more complex ESPHome automation logic while I designed and tested the hardware.

The heart of this system is the Automatic Transfer Switch (ATS). Mine is a two phase 200 Amp device that seems to have been originally made to work with a Generac generator. These are retailed for about $1,200. I bought mine on eBay for just under $500 shipped. It is a two-pole, two-way relay (DPDT). A grown up 8 contact relay. Under normal power conditions it takes the incoming utility power feeds coming from the meter and passes them to the house load center (breaker box). It also supplies the Generac generator with various feeds. For it to switch to generator power, the utility power needs to fail, the generator needs to be supplying 2 x 120VAC, 12VDC needs to be supplied to the ATS and lastly a quick pulse of about half a second needs to be applied between tag 23 and GND. At that point, it will switch to generator power which is passed to the house.

Return to utility power is automatic and instant upon power being present. The generator now needs to be shut down.

The system has a number of features:

  • Automatic detection of utility power failure and generator startup.
  • Automatic shutdown of generator upon utility power restoration.
  • Umbilical cable connection status between HA and generator
  • Session and total generator runtime stats.
  • Hours remaining until next generator service.
  • Automatic shutdown (optional) of generator at predetermined times with bypass.
  • Settable (optional) Quiet Hours when the generator will not start.
  • Overload shutdown after 20 seconds per phase (3,500 Watts + 5%).
  • Inrush current overload shutdown protection at 4375 Watts per phase.
  • Weekly or daily settable test schedule.
  • Generator functions (Start, Stop, Choke, etc.) in HA as switches.
  • Internal battery (9Ah SLA) to cover periods of no power to ESP32.
  • Two battery charges. One for the SLA battery in the controller. The other for the LiFePO4 battery on the generator. This is disconnected when the generator starts.
  • Optional generator battery charge current monitor via INA226
  • Optional external temperature monitor via MAX6675

This post is getting huge. Too long. I’ll stop now and try to answer questions as they come.

So here are the files and drawings which are in the next post due to size limitations.

Here is a link to Google Drive that has all the files.
https://drive.google.com/drive/folders/1QjLLpkDmho2loheWRxG_znKDYQaVos72?usp=drive_link

1 Like

When you start advising about installing a 200 amp transfer switch, I believe a little warning is necessary...

DANGER
No one here can see or check what your home wiring is. The best you will get is guesses, and you have no idea if the person 'helping' has any idea or not.
Keep in mind if you know what you are doing, I have no problem helping. But when you clearly do not know anything about the wiring you are looking at then PLEASE get help from someone that does.

Disclaimer

:warning: DANGER OF ELECTROCUTION :warning:

If your device connects to mains electricity (AC power) there is danger of electrocution if not installed properly. If you don't know how to install it, please call an electrician.

Beware: certain countries prohibit installation without a licensed electrician present

Remember: SAFETY FIRST. It is not worth the risk to yourself, your family and your home if you don't know exactly what you are doing. Never tinker or try to flash a device using the serial programming interface while it is connected to MAINS ELECTRICITY (AC power).

Here is the schematic and a couple of project photos.




Here is one of two files. This one has changes to configuration.yaml and some neccessary Helpers. The next post will have the ESP32 yaml.

# ============================================================================================
#  GENERATOR 8750W — MASTER PROJECT DOCUMENTATION
#  Predator 8750W / 7000W Inverter Generator — ESPHome / Home Assistant Automation
#  YAML developed with the assistance of Claude (claude.ai), Anthropic's AI assistant.
# ============================================================================================
#
#  FILE INDEX
#  ----------
#  SECTION 1 — PROJECT OVERVIEW & HELPER SETUP
#  SECTION 2 — HA HELPERS (input_number / template sensors / binary sensors)
#  SECTION 3 — ESPHOME FIRMWARE  (generator-8750w)
#  SECTION 4 — HOME ASSISTANT — configuration.yaml additions
#  SECTION 5 — LOVELACE CARDS
#    5a — Generator Entities Card  (main control panel)
#    5b — Generator Service Status Card
#    5c — Start Card  (hold-to-start button)
#    5d — Stop Card   (stop button)
#
# ============================================================================================


# ============================================================================================
#  SECTION 1 — PROJECT OVERVIEW & ONE-TIME HOME ASSISTANT SETUP
# ============================================================================================
#
# project_overview:
#   device:        Predator 8750W / 7000W Inverter Generator
#   controller:    ESP32 (esp32dev board) via ESPHome
#   relay_board:   XL9535 8-channel I2C relay board (address 0x20)
#   sensors:
#     - MAX6675 K-Type thermocouple (outdoor temperature, SPI)
#     - INA226 current sensor (battery charging current, I2C 0x40)
#     - CT clamps inside ATS (pulled from HA: Phase A & Phase B power)
#     - WiFi RSSI
#   ha_integration: ESPHome native API (encrypted)
#
# key_features:
#   - Automatic start on utility power loss (with quiet-hours block)
#   - Automatic stop on utility power restoration
#   - Configurable quiet hours (default 1 AM–8 AM) — toggleable from HA
#   - Nighttime auto-shutdown (3 attempts: 00:30, 01:30, 02:30)
#   - Weekly scheduled test run (configurable day/time)
#   - Overload protection per phase (startup inrush + normal thresholds)
#   - Session and total runtime tracking (persists across reboots via HA helper)
#   - Generator event log (searchable in HA history/logbook)
#   - Utility bypass switch for testing with utility power present
#   - Battery charger management (off while running, on when stopped)
#
# ============================================================================================
#  ONE-TIME HELPER CREATION — DO THIS BEFORE FLASHING FIRMWARE
# ============================================================================================
#
# required_helpers_to_create_manually_in_ha:
#
#   1. Generator Total Runtime  (input_number — stores runtime across reboots)
#      Path:  Settings → Devices & Services → Helpers → + Create Helper → Number
#      Name:           Generator Total Runtime
#      Entity ID:      input_number.generator_total_runtime
#      Minimum:        0
#      Maximum:        100000
#      Step size:      0.1
#      Unit:           h
#      NOTE: Set this value to match your generator's physical hour meter on first install.
#            Reset to 0 if replacing the generator.
#
#   2. Generator Service Interval Hours  (input_number — sets service interval)
#      Path:  Settings → Devices & Services → Helpers → + Create Helper → Number
#      Name:           Generator Service Interval Hours
#      Entity ID:      input_number.generator_service_interval_hours
#      Minimum:        1
#      Maximum:        500
#      Step size:      1
#      Unit:           h
#      Suggested default: 100  (adjust to your maintenance schedule)
#
#   3. Generator Hours Until Service  (template sensor)
#      Name:           Generator Hours Until Service
#      Entity ID:      sensor.generator_hours_until_service
#      Type:           Template
#      NOTE: Template definition is in SECTION 4 (configuration.yaml).
#
#   4. Generator Service Due  (template binary sensor)
#      Name:           Generator Service Due
#      Entity ID:      binary_sensor.generator_service_due
#      Type:           Template
#      NOTE: Template definition is in SECTION 4 (configuration.yaml).
#
#   5. Generator Service Life Remaining  (template sensor)
#      Name:           Generator Service Life Remaining
#      Entity ID:      sensor.generator_service_life_remaining
#      Type:           Template
#      NOTE: Template definition is in SECTION 4 (configuration.yaml).
#


# ============================================================================================
#  SECTION 2 — HA HELPERS REFERENCE TABLE
# ============================================================================================
#
# ha_helpers:
#
#   # --- Manually created in HA UI ---
#   - name:      Generator Total Runtime
#     entity_id: input_number.generator_total_runtime
#     type:      input_number
#     purpose:   Persists total engine hours across ESP32 reboots and reflashes.
#                Written by ESPHome on every generator stop. Set manually to match
#                the physical hour meter on first install.
#     unit:      h
#
#   - name:      Generator Service Interval Hours
#     entity_id: input_number.generator_service_interval_hours
#     type:      input_number
#     purpose:   User-configurable service interval. Used by template sensors to
#                calculate hours until next service and life-remaining percentage.
#     unit:      h
#
#   # --- Defined in configuration.yaml (SECTION 4) ---
#   - name:      Generator Hours Until Service
#     entity_id: sensor.generator_hours_until_service
#     type:      template sensor
#     purpose:   Calculates remaining hours until next scheduled service.
#                Displayed in both Lovelace cards.
#
#   - name:      Generator Service Due
#     entity_id: binary_sensor.generator_service_due
#     type:      template binary sensor
#     purpose:   Turns ON when hours_until_service <= 0 (service overdue).
#
#   - name:      Generator Service Life Remaining
#     entity_id: sensor.generator_service_life_remaining
#     type:      template sensor
#     purpose:   Percentage of service interval remaining (0–100%).
#                Displayed in the Generator Service Status card.
#


# ============================================================================================
#  SECTION 3 — ESPHOME FIRMWARE
#  File: generator-8750w.yaml   Flash via: ESPHome Dashboard or CLI
# ============================================================================================
#
# DIFF NOTE: Two versions of this file exist (Generator_8750W_yaml.txt and
# configuration_yaml_text.txt). The differences are minor:
#   - Weekly test section: configuration_yaml_text.txt has script.execute: generator_start
#     and script.execute: generator_stop commented out; Generator_8750W_yaml.txt calls
#     them directly.  The version below (Generator_8750W_yaml.txt) is canonical.
#   - K-probe and INA226 update_interval: configuration_yaml_text.txt uses 30s;
#     Generator_8750W_yaml.txt uses 300s (5 min). The 300s version is canonical.

Here is the yaml

esphome_firmware:
  _paste_into_file: "generator-8750w.yaml"
  _content: |

    # YAML developed with the assistance of Claude (claude.ai), Anthropic's AI assistant.
    #
    # =====================================================================
    #  BEFORE FLASHING THIS FIRMWARE — ONE-TIME HOME ASSISTANT SETUP
    # =====================================================================
    #  See SECTION 1 of this master documentation file.
    # =====================================================================

    substitutions:
      # =====================================================================
      #  USER-MODIFIABLE SETTINGS — change these without touching anything else
      # =====================================================================
      #
      # WEEKLY TEST SCHEDULE
      # Day:     MON TUE WED THU FRI SAT SUN
      # Hours:   0-23 (24h clock)   Minutes: 0-59   Seconds: 0-59
      #
      test_day:                       "WED"

      test_start_hour:                "8"
      test_start_minute:              "20"
      test_start_second:              "19"

      test_stop_hour:                 "8"
      test_stop_minute:               "21"
      test_stop_second:               "20"

      # Enable the weekly scheduled test run (true = enabled, false = disabled)
      weekly_test_enabled:            "true"
      #
      # =====================================================================
      # QUIET HOURS
      # Auto-start is BLOCKED between start time (inclusive) and end time (exclusive).
      # The frontend switch in HA can override this at any time.
      #
      quiet_hours_start:              "1"     # Hour to begin blocking  (0-23)
      quiet_minutes_start:            "00"    # Minute to begin blocking (0-59)

      quiet_hours_end:                "8"     # Hour to stop blocking   (0-23)
      quiet_minutes_end:              "00"    # Minute to stop blocking (0-59)

      # Enable quiet hours (true = block auto-start during quiet hours, false = allow any time)
      quiet_hours_enabled_default:    "true"
      #
      # =====================================================================
      # NIGHTTIME AUTO-SHUTDOWN
      # Three attempts to shut down if generator is still running late at night.
      # Each attempt checks whether the generator is actually running before acting.
      #
      shutdown_1_hour:                "0"     # First attempt  — default 00:30
      shutdown_1_minute:              "30"

      shutdown_2_hour:                "1"     # Second attempt — default 01:30
      shutdown_2_minute:              "30"

      shutdown_3_hour:                "2"     # Third attempt  — default 02:30
      shutdown_3_minute:              "30"

      # Enable nighttime auto-shutdown (true = enabled, false = disabled)
      nighttime_shutdown_enabled:     "true"
      #
      # =====================================================================
      # OVERLOAD PROTECTION
      # For Predator 8750/7000W. Adjust thresholds if using a different generator.
      #
      # Generator shuts down if either phase exceeds overload_watts continuously
      # for 20 seconds. Set 5% above rated continuous output (3500W + 5% = 3675W).
      # On startup a higher threshold applies for overload_startup_seconds to
      # allow inrush current (8750W peak / 2 phases = 4375W per phase).
      #
      overload_watts:                 "3675"  # Normal threshold in Watts per phase (running)
      overload_watts_startup:         "4375"  # Startup threshold in Watts per phase (inrush)
      overload_startup_seconds:       "20"    # Seconds to apply startup threshold after power on

      # Enable overload protection (true = enabled, false = disabled)
      overload_protection_enabled:    "true"
      #
      # =====================================================================
      # TIMEZONE — POSIX timezone string.
      #   Eastern:  EST5EDT,M3.2.0,M11.1.0
      #   Central:  CST6CDT,M3.2.0,M11.1.0
      #   Mountain: MST7MDT,M3.2.0,M11.1.0
      #   Pacific:  PST8PDT,M3.2.0,M11.1.0
      #   UTC:      UTC0
      #
      timezone:                       "EST5EDT,M3.2.0,M11.1.0"
      #
      # =====================================================================

    esphome:
      name: "generator-8750w"
      friendly_name: Generator 8750W
      on_boot:
        - priority: 600
          then:
            - switch.turn_off: r0_fuel               # Not used
            - switch.turn_off: r1_generator_esc
            - switch.turn_off: r2_generator_choke
            - switch.turn_off: r3_generator_start
            - switch.turn_off: r4_generator_stop
            - switch.turn_off: r5_battery_charger_12v
            - switch.turn_off: r6_ats_switch_over
            - switch.turn_off: r7_12v_to_ats
            - switch.turn_on: r5_battery_charger_12v  # Charge the generator battery on boot
        - priority: -100
          then:
            # Force runtime sensors to publish immediately after boot
            - component.update: sensor_session_runtime
            - component.update: sensor_total_runtime

    esp32:
      board: esp32dev
      framework:
        type: esp-idf

    logger:

    api:
      encryption:
        key: "n5GMxve4bK2OdPvy15Esm7CjsLn4jSmvzxzRP4tifCQ="

    ota:
      - platform: esphome
        password: "e07a4771d0cb8e40a321244c74c378bb"

    wifi:
      ssid: !secret wifi_IoT_ssid
      password: !secret wifi_IoT_password
      min_auth_mode: WPA
      power_save_mode: HIGH
      ap:
        ssid: "Generator-8750W Fallback Hotspot"
        password: "7Y2TxsnYWMn2"

    captive_portal:

    # ========================  GPIO PIN MAP  =========================================
    # GPIO05  K-Probe CS
    # GPIO12  GND from Generator — Umbilical cable connected
    # GPIO14  Detects 12V from generator when running
    # GPIO18  K-Probe CLK
    # GPIO19  K-Probe SO
    # GPIO21  I2C SDA
    # GPIO22  I2C SCL
    # GPIO25  Input — utility power failure detection (relay RA)
    # GPIO33  Input — generator power detection (relay RA)
    # =================================================================================

    # =====================================================================
    #  SCRIPTS — generator_start and generator_stop are the single
    #  authoritative sequences called from everywhere else in this file.
    #  mode: single means a second call is ignored if already running.
    # =====================================================================
    script:

      # ------------------------------------------------------------------
      # GENERATOR START
      # Starts the engine and transfers to generator power.
      # Transfer only occurs if utility is absent OR bypass switch is ON.
      # Resets session runtime counter; sets startup flag for inrush allowance.
      # ------------------------------------------------------------------
      - id: generator_start
        mode: single
        then:
          - lambda: 'id(session_runtime_minutes) = 0;'   # Reset session timer
          - lambda: 'id(generator_starting) = true;'     # Enable startup inrush allowance
          - delay: 2s
          - switch.turn_off: r1_generator_esc          # Turn OFF ESC to start
          - switch.turn_off: r5_battery_charger_12v    # Disconnect charger
          - switch.turn_on: r3_generator_start         # Start engine
          - delay: 4.0s
          - switch.turn_off: r3_generator_start        # Release starter
          - delay: 1s                                  # Engine should be running
          - switch.turn_on: r1_generator_esc           # ESC on for run
          # Transfer to generator if utility power absent OR bypass switch is ON
          - if:
              condition:
                or:
                  - binary_sensor.is_off: utility_power_on
                  - switch.is_on: utility_bypass_switch
              then:
                - switch.turn_on: r7_12v_to_ats        # 12V to ATS
                - delay: 1s                            # Stabilize
                - switch.turn_on: r6_ats_switch_over   # Transfer to Generator
                - delay: 0.5s                          # Momentary pulse
                - switch.turn_off: r6_ats_switch_over  # Done
          # Wait for inrush period then drop to normal threshold
          - delay: !lambda 'return ${overload_startup_seconds} * 1000;'
          - lambda: |-
              id(generator_starting) = false;
              ESP_LOGI("overload", "Startup inrush period ended - normal overload threshold now active (%d W)", ${overload_watts});
          # Total ~8.5 seconds from call to generator power

      # ------------------------------------------------------------------
      # GENERATOR STOP
      # Stops the engine and returns all switches to default positions.
      # Saves final session minutes to HA total runtime before stopping.
      # Always resets utility bypass switch and startup flag.
      # ------------------------------------------------------------------
      - id: generator_stop
        mode: single
        then:
          # Save session runtime to HA total before stopping
          - homeassistant.service:
              service: input_number.set_value
              data_template:
                entity_id: input_number.generator_total_runtime
                value: !lambda |-
                  return id(total_runtime_hours).state + (id(session_runtime_minutes) / 60.0);
          - delay: 5s
          - switch.turn_off: r1_generator_esc          # Turn OFF the ESC switch
          - delay: 0.5s
          - switch.turn_on: r4_generator_stop          # Stop electrically
          - delay: 6s                                  # Hold until stopped
          - switch.turn_off: r4_generator_stop
          # Return all switches/relays to default positions
          - switch.turn_off: r3_generator_start
          - switch.turn_on: r5_battery_charger_12v    # Resume charging
          - switch.turn_off: r6_ats_switch_over
          - switch.turn_off: r7_12v_to_ats
          - switch.turn_off: r2_generator_choke
          - switch.turn_off: r1_generator_esc
          - switch.turn_off: utility_bypass_switch    # Reset bypass on every shutdown
          - lambda: 'id(session_runtime_minutes) = 0;'  # Reset session timer after stop
          - lambda: 'id(generator_starting) = false;'   # Ensure startup flag is cleared

    # ============================  SNTP TIME  =====================================
    time:
      - platform: sntp
        id: sntp_time
        timezone: ${timezone}
        on_time:

          # ----------------------  WEEKLY TEST  --------------------------------
          - days_of_week: ${test_day}
            hours: ${test_start_hour}
            minutes: ${test_start_minute}
            seconds: ${test_start_second}
            then:
              - if:
                  condition:
                    lambda: 'return ${weekly_test_enabled};'
                  then:
                    - logger.log:
                        level: INFO
                        format: "Weekly Scheduled Test BEGIN"
                    - text_sensor.template.publish:
                        id: generator_last_event
                        state: "Weekly Scheduled Test BEGIN"
                    - switch.turn_on: r0_fuel
                  else:
                    - logger.log:
                        level: INFO
                        format: "Weekly Scheduled Test SKIPPED - (weekly_test_enabled is 'false')"
                    - text_sensor.template.publish:
                        id: generator_last_event
                        state: "Weekly Scheduled Test SKIPPED"

          - days_of_week: ${test_day}
            hours: ${test_stop_hour}
            minutes: ${test_stop_minute}
            seconds: ${test_stop_second}
            then:
              - if:
                  condition:
                    lambda: 'return ${weekly_test_enabled};'
                  then:
                    - logger.log:
                        level: INFO
                        format: "Weekly Scheduled Test END"
                    - text_sensor.template.publish:
                        id: generator_last_event
                        state: "Weekly Scheduled Test END"
                    - switch.turn_off: r0_fuel

          # ----------------------  NIGHTTIME AUTO SHUTDOWN  --------------------

          # First attempt
          - hours: ${shutdown_1_hour}
            minutes: ${shutdown_1_minute}
            seconds: 0
            then:
              - if:
                  condition:
                    lambda: 'return ${nighttime_shutdown_enabled};'
                  then:
                    - if:
                        condition:
                          binary_sensor.is_on: generator_12v_running
                        then:
                          - logger.log:
                              level: INFO
                              format: "Nighttime shutdown - attempt 1 trigger"
                          - text_sensor.template.publish:
                              id: generator_last_event
                              state: "Nighttime shutdown - attempt 1 trigger"
                          - script.execute: generator_stop
                  else:
                    - logger.log:
                        level: INFO
                        format: "Nighttime shutdown attempt 1 SKIPPED"
                    - text_sensor.template.publish:
                        id: generator_last_event
                        state: "Nighttime shutdown attempt 1 SKIPPED"

          # Second attempt
          - hours: ${shutdown_2_hour}
            minutes: ${shutdown_2_minute}
            seconds: 0
            then:
              - if:
                  condition:
                    lambda: 'return ${nighttime_shutdown_enabled};'
                  then:
                    - if:
                        condition:
                          binary_sensor.is_on: generator_12v_running
                        then:
                          - logger.log:
                              level: INFO
                              format: "Nighttime shutdown - attempt 2 trigger"
                          - text_sensor.template.publish:
                              id: generator_last_event
                              state: "Nighttime shutdown - attempt 2 trigger"
                          - script.execute: generator_stop
                  else:
                    - logger.log:
                        level: INFO
                        format: "Nighttime shutdown attempt 2 SKIPPED"
                    - text_sensor.template.publish:
                        id: generator_last_event
                        state: "Nighttime shutdown attempt 2 SKIPPED"

          # Third attempt
          - hours: ${shutdown_3_hour}
            minutes: ${shutdown_3_minute}
            seconds: 0
            then:
              - if:
                  condition:
                    lambda: 'return ${nighttime_shutdown_enabled};'
                  then:
                    - if:
                        condition:
                          binary_sensor.is_on: generator_12v_running
                        then:
                          - logger.log:
                              level: INFO
                              format: "Nighttime shutdown - attempt 3 trigger"
                          - text_sensor.template.publish:
                              id: generator_last_event
                              state: "Nighttime shutdown - attempt 3 trigger"
                          - script.execute: generator_stop
                  else:
                    - logger.log:
                        level: INFO
                        format: "Nighttime shutdown attempt 3 SKIPPED"
                    - text_sensor.template.publish:
                        id: generator_last_event
                        state: "Nighttime shutdown attempt 3 SKIPPED"

          # ----------------------  RUNTIME COUNTER (every minute)  -------------
          - seconds: 0
            then:
              - if:
                  condition:
                    binary_sensor.is_on: generator_12v_running
                  then:
                    - lambda: 'id(session_runtime_minutes) += 1;'
                    - component.update: sensor_session_runtime
                    - component.update: sensor_total_runtime

    # =======================  K-PROBE TEMPERATURE SENSOR (SPI)  ==================
    spi:
      miso_pin: GPIO19   # SO
      clk_pin: GPIO18    # CLK

    # =======================  I2C BUS  ===========================================
    i2c:
      scl: GPIO22
      sda: GPIO21
      scan: True
      frequency: 100kHz

    # =======================  GLOBALS  ===========================================
    globals:
      - id: overload_seconds          # Overload protection running counter
        type: int
        restore_value: no
        initial_value: '0'
      - id: quiet_hours_enabled       # Runtime toggle; initialised from substitution
        type: bool
        restore_value: no
        initial_value: '${quiet_hours_enabled_default}'
      - id: session_runtime_minutes   # Minutes since last generator start; resets on start/stop
        type: int
        restore_value: no
        initial_value: '0'
      - id: generator_starting        # True during inrush period after start
        type: bool
        restore_value: no
        initial_value: 'false'

    # =======================  EVENT LOG SENSOR  ==================================
    text_sensor:
      - platform: template
        name: "Generator Last Event"
        id: generator_last_event

    # =======================  SENSORS  ===========================================
    sensor:

      # Current session runtime (resets to 0 each start)
      - platform: template
        name: "Generator Session Runtime"
        id: sensor_session_runtime
        unit_of_measurement: "h"
        device_class: duration
        state_class: measurement
        accuracy_decimals: 2
        lambda: 'return id(session_runtime_minutes) / 60.0;'
        update_interval: 60s

      # Total accumulated runtime pulled from HA input_number helper
      - platform: homeassistant
        name: "Generator Total Runtime"
        id: total_runtime_hours
        entity_id: input_number.generator_total_runtime
        unit_of_measurement: "h"

      # Running total (session + stored) displayed in HA
      - platform: template
        name: "Generator Total Runtime Display"
        id: sensor_total_runtime
        unit_of_measurement: "h"
        device_class: duration
        state_class: total_increasing
        accuracy_decimals: 2
        lambda: 'return id(total_runtime_hours).state + (id(session_runtime_minutes) / 60.0);'
        update_interval: 60s

      # MAX6675 K-Type thermocouple — outdoor temperature
      - platform: max6675
        id: k_probe_controlbox
        name: "Outdoor Temperature"
        cs_pin: GPIO5
        unit_of_measurement: "°C"
        device_class: temperature
        state_class: measurement
        update_interval: 300s

      # INA226 — generator battery charging current
      - platform: ina226
        address: 0x40
        shunt_resistance: 0.1 ohm
        max_current: 3.2A
        adc_time: 140us
        adc_averaging: 128
        update_interval: 300s
        current:
          id: generator_battery_charging_current
          name: "Generator Battery Charging Current"

      # WiFi signal strength
      - platform: wifi_signal
        id: generator_wifi_db
        name: "Generator WiFi Signal"
        update_interval: 900s

      # CT Clamp Phase A — pulled from HA (located inside ATS)
      - platform: homeassistant
        id: ct_clamp_phase_a
        entity_id: sensor.ats_generator_output_power_a
        unit_of_measurement: "W"

      # CT Clamp Phase B — pulled from HA (located inside ATS)
      - platform: homeassistant
        id: ct_clamp_phase_b
        entity_id: sensor.ats_home_power_b
        unit_of_measurement: "W"

    # =======================  OVERLOAD PROTECTION  ===============================
    # Checks every 10 seconds.
    # Startup threshold (overload_watts_startup) applies for overload_startup_seconds
    # after start to allow inrush. Normal threshold (overload_watts) applies after.
    # Shuts down after 20 seconds sustained overload at whichever threshold is active.
    interval:
      - interval: 10s
        then:
          - if:
              condition:
                lambda: 'return ${overload_protection_enabled};'
              then:
                - if:
                    condition:
                      or:
                        - lambda: 'return id(generator_starting) && (id(ct_clamp_phase_a).state > ${overload_watts_startup} || id(ct_clamp_phase_b).state > ${overload_watts_startup});'
                        - lambda: 'return !id(generator_starting) && (id(ct_clamp_phase_a).state > ${overload_watts} || id(ct_clamp_phase_b).state > ${overload_watts});'
                    then:
                      - lambda: |-
                          id(overload_seconds) += 10;
                          ESP_LOGW("overload", "Phase overload detected! Counter: %d seconds (startup=%s, threshold=%dW)",
                            id(overload_seconds),
                            id(generator_starting) ? "yes" : "no",
                            id(generator_starting) ? ${overload_watts_startup} : ${overload_watts});
                      - if:
                          condition:
                            lambda: 'return id(overload_seconds) >= 20;'
                          then:
                            - logger.log:
                                level: ERROR
                                format: "OVERLOAD SHUTDOWN - phase exceeded limit for 20 seconds"
                            - text_sensor.template.publish:
                                id: generator_last_event
                                state: "OVERLOAD SHUTDOWN - phase exceeded limit for 20 seconds"
                            - script.execute: generator_stop
                            - lambda: 'id(overload_seconds) = 0;'
                    else:
                      - lambda: 'id(overload_seconds) = 0;'

    # =======================  XL9535 I2C RELAY BOARD  ============================
    xl9535:
      - id: xl9535_hub
        address: 0x20

    # =======================  SWITCHES  ==========================================
    switch:

      # Reboot ESP32
      - platform: restart
        name: 'REBOOT ESP32'

      # Quiet Hours Toggle
      # ON  = auto-start BLOCKED between quiet_hours_start and quiet_hours_end (normal/security)
      # OFF = auto-start allowed any hour (emergency override)
      - platform: template
        name: "Quiet Hours Auto-Start Block (1AM–8AM)"
        id: quiet_hours_switch
        optimistic: true
        restore_mode: ALWAYS_ON
        turn_on_action:
          - lambda: 'id(quiet_hours_enabled) = true;'
        turn_off_action:
          - lambda: 'id(quiet_hours_enabled) = false;'

      # Utility Bypass Switch
      # ON  = generator starts and transfers even with utility present (for testing)
      # OFF = normal operation (transfer only on utility loss)
      # Resets to OFF on every reboot and every generator stop.
      - platform: template
        name: "Utility Bypass (run on generator with utility present)"
        id: utility_bypass_switch
        optimistic: true
        restore_mode: ALWAYS_OFF
        turn_on_action:
          - logger.log:
              level: INFO
              format: "Utility bypass ENABLED - generator will transfer even with utility present"
          - text_sensor.template.publish:
              id: generator_last_event
              state: "Utility bypass ENABLED"
        turn_off_action:
          - logger.log:
              level: INFO
              format: "Utility bypass DISABLED - normal operation resumed"
          - text_sensor.template.publish:
              id: generator_last_event
              state: "Utility bypass DISABLED"

      # Relay 0 — Fuel line valve (NOT USED)
      - platform: gpio
        id: r0_fuel
        name: "R0 Fuel"
        pin:
          xl9535: xl9535_hub
          number: 0
          mode: { output: true }
          inverted: false

      # Relay 1 — Generator ESC switch (OFF to start, ON once running)
      - platform: gpio
        id: r1_generator_esc
        name: "Generator ESC Switch (on when run)"
        pin:
          xl9535: xl9535_hub
          number: 1
          mode: { output: true }
          inverted: false

      # Relay 2 — Choke / start trigger (on_turn_on calls generator_start script)
      - platform: gpio
        id: r2_generator_choke
        name: "Generator Choke (on to start)"
        pin:
          xl9535: xl9535_hub
          number: 2
          mode: { output: true }
          inverted: false
        on_turn_on:
          - script.execute: generator_start

      # Relay 3 — Starter solenoid (auto-releases after 4 s)
      - platform: gpio
        id: r3_generator_start
        name: "Generator Start"
        pin:
          xl9535: xl9535_hub
          number: 3
          mode: { output: true }
          inverted: false
        on_turn_on:
          - delay: 4.0s
          - switch.turn_off: r3_generator_start

      # Relay 4 — Stop the generator (on_turn_on calls generator_stop script)
      - platform: gpio
        id: r4_generator_stop
        name: "Generator Stop"
        pin:
          xl9535: xl9535_hub
          number: 4
          mode: { output: true }
          inverted: false
        on_turn_on:
          - script.execute: generator_stop

      # Relay 5 — Battery charger (OFF while generator running, ON when stopped)
      - platform: gpio
        id: r5_battery_charger_12v
        name: "Generator Battery Charger (off when run)"
        pin:
          xl9535: xl9535_hub
          number: 5
          mode: { output: true }
          inverted: false

      # Relay 6 — ATS transfer (momentary 0.5 s pulse)
      - platform: gpio
        id: r6_ats_switch_over
        name: "Transfer (momentary contact)"
        pin:
          xl9535: xl9535_hub
          number: 6
          mode: { output: true }
          inverted: false
        on_turn_on:
          - delay: 0.5s
          - switch.turn_off: r6_ats_switch_over

      # Relay 7 — +12V to ATS (energised prior to transfer)
      - platform: gpio
        id: r7_12v_to_ats
        name: "12V to ATS"
        pin:
          xl9535: xl9535_hub
          number: 7
          mode: { output: true }
          inverted: false

    # =======================  BINARY SENSORS  ====================================
    binary_sensor:

      # Utility power detection via relay RA — GPIO25
      # on_press   = utility RESTORED → stop generator (unless bypass active)
      # on_release = utility LOST     → start generator (subject to quiet hours)
      - platform: gpio
        device_class: power
        pin:
          number: GPIO25
          mode: { input: true, pullup: true }
          inverted: true
        id: utility_power_on
        name: "Utility Power"

        on_press:
          - if:
              condition:
                switch.is_on: utility_bypass_switch
              then:
                - logger.log:
                    level: INFO
                    format: "Utility power restored but bypass is ON - generator kept running"
                - text_sensor.template.publish:
                    id: generator_last_event
                    state: "Utility restored - bypass ON - generator kept running"
              else:
                - logger.log:
                    level: INFO
                    format: "Utility power restored - stopping generator"
                - text_sensor.template.publish:
                    id: generator_last_event
                    state: "Utility power restored - stopping generator"
                - script.execute: generator_stop

        on_release:
          - if:
              condition:
                lambda: |-
                  if (!id(quiet_hours_enabled)) return true;
                  auto time = id(sntp_time).now();
                  int now_mins   = time.hour * 60 + time.minute;
                  int start_mins = ${quiet_hours_start} * 60 + ${quiet_minutes_start};
                  int end_mins   = ${quiet_hours_end}   * 60 + ${quiet_minutes_end};
                  return !(now_mins >= start_mins && now_mins < end_mins);
              then:
                - logger.log:
                    level: INFO
                    format: "Utility lost - starting generator (quiet hours check passed)"
                - text_sensor.template.publish:
                    id: generator_last_event
                    state: "Utility lost - starting generator"
                - script.execute: generator_start
              else:
                - logger.log:
                    level: INFO
                    format: "Utility lost - generator auto-start BLOCKED by quiet hours"
                - text_sensor.template.publish:
                    id: generator_last_event
                    state: "Utility lost - generator auto-start BLOCKED by quiet hours"

      # Generator power detection via relay RA — GPIO33
      - platform: gpio
        device_class: power
        pin:
          number: GPIO33
          mode: { input: true, pullup: true }
          inverted: true
        id: gnerator_power_on
        name: "Generator Power"
        on_press:
          - logger.log:
              level: INFO
              format: "Generator power sensor triggered - starting generator"
          - text_sensor.template.publish:
              id: generator_last_event
              state: "Generator power sensor triggered - starting generator"
          - script.execute: generator_start

      # Umbilical cable detection — GPIO12
      - platform: gpio
        pin:
          number: GPIO12
          mode: { input: true, pullup: true }
          inverted: true
        id: umbilical_connection
        name: "Umbilical Cable"
        icon: mdi:cable-data
        device_class: connectivity

      # Generator 12V running detection — GPIO14
      - platform: gpio
        pin:
          number: GPIO14
          mode: { input: true, pullup: true }
          inverted: true
        id: generator_12v_running
        name: "Generator Running"

Lastly some Lovelace cards that I use with it.



# ============================================================================================
#  SECTION 4 — HOME ASSISTANT configuration.yaml ADDITIONS
#  Add these blocks to your existing configuration.yaml (or appropriate package file).
# ============================================================================================

ha_configuration_yaml_additions:
  _instructions: >
    Add the template block below to your configuration.yaml.
    If you already have a 'template:' key, merge these entries under it.
    Restart Home Assistant after making changes.

  template:

    # ------------------------------------------------------------------
    # SENSOR: Hours until next service
    # Computes remaining hours = service_interval - total_runtime
    # Negative values mean service is overdue.
    # ------------------------------------------------------------------
    - sensor:
        - name: "Generator Hours Until Service"
          unique_id: generator_hours_until_service
          unit_of_measurement: "h"
          state_class: measurement
          state: >-
            {% set total = states('sensor.generator_8750w_generator_total_runtime_display') | float(0) %}
            {% set interval = states('input_number.generator_service_interval_hours') | float(100) %}
            {{ (interval - total) | round(1) }}
          availability: >-
            {{ states('sensor.generator_8750w_generator_total_runtime_display') not in ['unknown','unavailable']
               and states('input_number.generator_service_interval_hours') not in ['unknown','unavailable'] }}

    # ------------------------------------------------------------------
    # BINARY SENSOR: Service due flag
    # ON when hours_until_service <= 0 (service overdue)
    # ------------------------------------------------------------------
    - binary_sensor:
        - name: "Generator Service Due"
          unique_id: generator_service_due
          device_class: problem
          state: >-
            {{ states('sensor.generator_hours_until_service') | float(1) <= 0 }}
          availability: >-
            {{ states('sensor.generator_hours_until_service') not in ['unknown','unavailable'] }}

    # ------------------------------------------------------------------
    # SENSOR: Service life remaining (percentage)
    # 100% = just serviced, 0% = interval fully consumed (service due)
    # ------------------------------------------------------------------
    - sensor:
        - name: "Generator Service Life Remaining"
          unique_id: generator_service_life_remaining
          unit_of_measurement: "%"
          state_class: measurement
          state: >-
            {% set total    = states('sensor.generator_8750w_generator_total_runtime_display') | float(0) %}
            {% set interval = states('input_number.generator_service_interval_hours') | float(100) %}
            {% set pct = ((interval - total) / interval * 100) | round(1) %}
            {{ [pct, 0] | max }}
          availability: >-
            {{ states('sensor.generator_8750w_generator_total_runtime_display') not in ['unknown','unavailable']
               and states('input_number.generator_service_interval_hours') not in ['unknown','unavailable'] }}


# ============================================================================================
#  SECTION 5 — LOVELACE CARDS
#  Paste each card definition into the Lovelace YAML editor for the relevant card.
# ============================================================================================


# --------------------------------------------------------------------------------------------
#  SECTION 5a — GENERATOR ENTITIES CARD  (main control panel)
#  Title: Generator 8750 / 7000W
#  Background: #200060  Text: cyan
# --------------------------------------------------------------------------------------------

lovelace_generator_entities_card:
  _paste_into: "Lovelace → Edit Dashboard → Add Card → Manual Card"
  _content: |

    type: entities
    entities:
      - type: custom:text-divider-row
        text: POWER SOURCE
      - entity: binary_sensor.generator_8750w_utility_power
        name: Utility Power
        show_state: true
        state_color: true
        card_mod:
          style: |
            hui-generic-entity-row {
              color:
                {% if is_state('binary_sensor.generator_8750w_utility_power','on') %}
                  lime
                {% else %}
                  crimson
                {% endif %};
            }
      - entity: binary_sensor.generator_8750w_generator_power
        name: Generator Power
        show_state: true
        state_color: true
        card_mod:
          style: |
            hui-generic-entity-row {
              color:
                {% if is_state('binary_sensor.generator_8750w_generator_power','on') %}
                  lime
                {% else %}
                  firebrick
                {% endif %};
            }
      - type: custom:text-divider-row
        text: GENERATOR STATUS
      - entity: binary_sensor.generator_8750w_umbilical_cable
        name: Umbilical Cable
        show_state: true
        state_color: true
        card_mod:
          style: |
            :host {
              color: lime;
              }
            {% if is_state('binary_sensor.generator_8750w_umbilical_cable','off') %}
            hui-generic-entity-row {
              animation: flash 1s linear infinite;
            }
            @keyframes flash {
              0% { color: white; }
              50% { color: white; }
              100% { color: red; }
            }
            {% else %}
                --paper-item-icon-color: ;
                color: steelblue;
              {% endif %}
            }
      - entity: binary_sensor.generator_8750w_generator_running
        name: Generator Running
        show_state: true
        state_color: true
        card_mod:
          style: |
            hui-generic-entity-row {
              color:
                {% if is_state('binary_sensor.generator_8750w_generator_running','on') %}
                  lime
                {% else %}
                  firebrick
                {% endif %};
            }
      - entity: sensor.generator_8750w_generator_session_runtime
        name: Generator Session Runtime
      - entity: sensor.generator_8750w_generator_total_runtime_display
        name: Generator Total Runtime Display
      - entity: sensor.generator_hours_until_service
        name: Hours Until Next Service
        card_mod:
          style: |
            :host {
              color: burlywood;
              }
      - entity: switch.generator_8750w_reboot_esp32
        name: REBOOT ESP32
        card_mod:
          style: |
            :host {
              color: darkorange;
              }
      - entity: switch.generator_8750w_quiet_hours_auto_start_block_1am_8am
        name: Quiet Hours Auto-Start Block (1AM–8AM)
        card_mod:
          style: |
            :host {
              color: white;
              }
      - entity: sensor.generator_8750w_generator_wifi_signal
        name: Generator WiFi Signal
        card_mod:
          style: |
            :host {
              color: darksalmon;
              }
      - entity: sensor.generator_8750w_outdoor_temperature
        name: Outdoor Temperature
        card_mod:
          style: |
            :host {
              color: lightgreen;
              }
      - type: custom:text-divider-row
        text: GENERATOR FUNCTIONS
      - type: custom:button-card
        entity: switch.generator_8750w_generator_start
        name: Generator Start - Press, Hold & Release
        show_name: true
        show_icon: true
        show_state: true
        state_color: true
        tap_action:
          action: none
        hold_action:
          action: toggle
          confirmation:
            text: Start the Generator?
        styles:
          card:
            - background: transparent
            - box-shadow: none
            - border-bottom: 1px solid rgba(255,255,255,0.1)
            - height: 52px
            - padding: 0 16px 0 4px
            - margin: 0
          grid:
            - grid-template-areas: '"i n s"'
            - grid-template-columns: 40px 1fr auto
            - grid-template-rows: 52px
            - align-items: center
            - align-content: center
            - justify-content: start
          img_cell:
            - align-self: center
            - justify-content: flex-start
            - height: 52px
            - margin-left: 0px
          icon:
            - width: 24px
            - height: 24px
            - align-self: center
            - justify-self: flex-start
            - color: |
                [[[ return entity.state === 'on' ? '#00FF7F' : '#35729b'; ]]]
          name:
            - font-size: 14px
            - font-weight: 400
            - color: cyan
            - text-align: left
            - justify-self: start
            - padding-left: 8px
            - align-self: center
          state:
            - font-size: 13px
            - text-align: right
            - align-self: center
            - color: |
                [[[ return entity.state === 'on' ? '#ffc23e' : 'yellow'; ]]]
      - entity: switch.generator_8750w_generator_stop
        name: Generator Stop
        show_state: true
        state_color: true
      - entity: switch.generator_8750w_12v_to_ats
        name: 12V to ATS
        show_state: true
        state_color: true
      - entity: switch.generator_8750w_transfer_momentary_contact
        name: Transfer (momentary contact)
        show_state: true
        state_color: true
        state_icons:
          "off": mdi:shield-home-outline
          "on": mdi:shield-home
      - entity: switch.generator_8750w_generator_esc_switch_on_when_run
        name: Generator ESC Switch (on when run)
        show_state: true
        state_color: true
      - entity: switch.generator_8750w_generator_battery_charger_off_when_run
        name: Generator Battery Charge (off when run)
        show_state: true
        state_color: true
        state_icons:
          "off": mdi:shield-home-outline
          "on": mdi:shield-home
      - entity: sensor.generator_8750w_generator_battery_charging_current
        name: Generator Battery Charging Current
    title: Generator 8750 / 7000W
    show_header_toggle: false
    card_mod:
      style: |
        ha-card {
          color: cyan;
          --ha-card-background: #200060;
        }


# --------------------------------------------------------------------------------------------
#  SECTION 5b — GENERATOR SERVICE STATUS CARD
# --------------------------------------------------------------------------------------------

lovelace_generator_service_status_card:
  _paste_into: "Lovelace → Edit Dashboard → Add Card → Manual Card"
  _content: |

    type: entities
    title: Generator Service Status
    entities:
      - entity: sensor.generator_hours_until_service
        name: Hours Until Next Service
      - entity: sensor.generator_service_life_remaining
        name: Service Life Remaining
      - entity: input_number.generator_service_interval_hours
        name: Service Interval (hours)
    card_mod:
      style: |
        ha-card {
          color: cyan;
          --ha-card-background: #200060;
        }


# --------------------------------------------------------------------------------------------
#  SECTION 5c — START CARD  (hold to start)
# --------------------------------------------------------------------------------------------

lovelace_start_card:
  _paste_into: "Lovelace → Edit Dashboard → Add Card → Manual Card"
  _content: |

    show_name: true
    show_icon: true
    type: button
    entity: switch.generator_8750w_generator_choke_on_to_start
    name: "Start Generator:   Press, Hold and Release"
    icon: mdi:generator-portable
    tap_action:
      action: none
    hold_action:
      action: call-service
      service: switch.turn_on
      target:
        entity_id: switch.generator_8750w_generator_choke_on_to_start
      confirmation:
        text: Start Generator?
    card_mod:
      style: |
        :host {
          --state-switch-on-color: #00FF7F;
          --state-switch-off-color: yellow;
          --ha-card-background: #300030;
        }


# --------------------------------------------------------------------------------------------
#  SECTION 5d — STOP CARD
# --------------------------------------------------------------------------------------------

lovelace_stop_card:
  _paste_into: "Lovelace → Edit Dashboard → Add Card → Manual Card"
  _content: |

    show_name: true
    show_icon: true
    type: button
    entity: switch.generator_8750w_generator_stop
    name: Stop Generator
    icon: mdi:generator-portable
    card_mod:
      style: |
        :host {
          --state-switch-on-color: #00FF0F;
          --state-switch-off-color: #ff0000;
          --ha-card-background: #300030;
        }


# ============================================================================================
#  END OF MASTER DOCUMENTATION FILE
#  Generator 8750W Automation Project
#  Generated: 2026-05-15
# ============================================================================================

Thank you. I already said that in UPPER CASE. Your re-enforcement is most welcome.

1 Like

An inspiring project that seems to have a lot of teething issues addressed.

This is my third go at doing this. I really wanted a complete project and once I had the hardware down right, the software took about four months to complete. First I got it solid and then I added features. I used Claude when I knew there was going to be both jinja and C++ involved. Claude was great. However, the idea and concept was mine.

The nice thing about is that it works.

This is the key. There are many ideas and concepts here that will form the basis of other people's solutions.

My HA system has a large UPS on it, an APC Smart-UPS 2200 XL which holds my internet, LAN HA, PoE cameras, etc. for 4.50 hours. Even so, of I shut the generator down at night, HA will be dead in the morning. The 9Ah SLA battery in my generator controller keeps that alive for over 48 hours as it draws just under 150mA. 9 / 0.15 = 60 hours. The battery is not bew so say 48 hours.

This means that I have no remote control of the generator in the morning. I need to go an press the generator Start button on the generator once per day. I could, I suppose design a remote start device. I thought about this an decided not to because I would need live electronics on the generator which would need to be powered (runs the generator battery down) and would be subject to the generator's vibration. So I decided against doing this. I walk out and press the Start button manually once per day.

The ONE THING I wanted was that the generator would start if the utility power failed regardless of the state of HA. Yes, the ESP32 talks to HA but it will poerform its prime function of clicking relays and starting or stopping the generator regardless. That is why the 9Ah 12V SLA is there. Utility off, suddenly there is no power. The key is the diode cominhg from the controller battery to the 12V power supply. The SLA battery, when fully charged produces 13.2V (6 x 2,2V). The diode has a voltage drop of just over 1V. I set my power supply to 14.25V and connect that to the LM2596 which can take input up to 40V. If the utility is on, current flows from the 14.25V power supply. If the utilty power drops out and the power supply stops delivering power, current now flows througe the 20A diode. The battery is at 13.2V and I lose 1V acreoss the diode so the LM2596 gets roughly 12V. The LM2596 is set to produce 5V so I never lose power to the ESP32. Simple but effective.