Starting a Diesel generator with ESPHome

First some background on what I am doing: I have somewhat unreliable power and have put together a Diesel backup generator. This would be fairly simple to set up with an Arduino, but I am trying to wrap my head around how to achieve it with ESPHome. I am going to eventually move this to it’s own controller but currently I am abusing the load shedding relays on my transfer switch to control the generator. I have been able to successfully start the generator my manually toggling the load shedding relays which I have attached to the power and starter (and soon the glowplugs), so the hardware is ready. I also have an input that is mostly able to tell if the engine is running by pulse counting the output of the W terminal of the alternator. The ATS has the ability to tell if power is present on the output, the power from the power company, and the generator. The final unit will be able to read the coolant temp sensor of the generator as well as oil pressure

My end-goal is to:

  • detect loss of power from the power company
  • check the current temp
  • run the glowplugs for a time based on temp
  • activate power to the fuel solenoid and alternator regulator
  • activate the starter and monitor the pulses from the alternator until they rise above a threshold or we have been cranking too long (in which case we wait for a while and try again)
  • give the generator some time to warm up
  • verify power is still out and if so cut power over using the transfer switch
  • monitor power and if it comes back verify it is stable before cutting back over
  • let the generator cool down before shutting it down

I would like this to live on the ESP. Home assistant should be able to override initiating the transfer as well as overriding cutting back to grid power, but I want this to be fully autonomous if HA is unavailable.

This is the basic IO stuff I have started with:

esphome:
  name: ats

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: 

ota:
  password: 

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Ats Fallback Hotspot"
    password: 

captive_portal:

web_server:
  port: 80
  auth:
    username: !secret esphome_web_username
    password: !secret esphome_web_password
        

sensor:
    - platform: pulse_counter
      pin: GPIO32
      name: "Generator RPM"
      update_interval: 200ms
    - platform: adc
      pin: GPIO39
      name: "Voltage Out"
      id: v_1
      attenuation: auto
      update_interval: 10ms
      filters:
        - calibrate_linear:
            - 1.8 -> 0.0
            - 2.77 -> 246.1
            - 2.78 -> 247.6
        - max:
            window_size: 500
            send_every: 100
            send_first_at: 100
    - platform: adc
      pin: GPIO34
      name: "Voltage Generator"
      id: gen_v
      attenuation: auto
      update_interval: 10ms
      filters:
        - calibrate_linear:
            - 1.805 -> 0.0
            - 2.77 -> 246.1
            - 2.78 -> 247.6
        - max:
            window_size: 500
            send_every: 100
            send_first_at: 100
    - platform: adc
      pin: GPIO35
      name: "Voltage RMP" # Power Company
      id: v_3
      attenuation: auto
      update_interval: 10ms
      filters:
        - calibrate_linear:
            - 1.8 -> 0.0
            - 2.78 -> 245.1
            - 2.79 -> 246.6
        - max:
            window_size: 500
            send_every: 100
            send_first_at: 100
binary_sensor:
  - platform: template
    name: "Power State Generator"
    icon: mdi:engine
    lambda: !lambda |-
      if (id(gen_v).state > 200) {
        return 1;
      } else {
        return 0;
      }
  - platform: template
    name: "Power State Out"
    lambda: !lambda |-
      if (id(v_1).state > 200) {
        return 1;
      } else {
        return 0;
      }
  - platform: template
    name: "Power State RMP"
    lambda: !lambda |-
      if (id(v_3).state > 200) {
        return 1;
      } else {
        return 0;
      }


switch:
  - platform: gpio
    pin: GPIO33
    name: "TS Relay"
  - platform: gpio
    pin: GPIO27
    name: "Relay A" # Glow Plugs
  - platform: gpio
    pin: GPIO14
    name: "Relay B" # Starter
  - platform: gpio
    pin: GPIO25
    name: "Relay C" # Fuel shutoff
  - platform: gpio
    pin: GPIO26
    name: "Relay D"
  - platform: gpio
    pin: GPIO13
    name: "HVACA"
  - platform: gpio
    pin: GPIO16
    name: "HVACB"
2 Likes

Dis you ever get this to work?

Can you share what you did?

Hello there, any update on this project? looks promising.
Regards

Yes, It has successfully been working, it has also stepped in for a couple of outages. The one tweak I had to make was to make the controls invisible to google assistant, as my young son told google to “turn everything on” and ran the generator for 6 hours while I was at work :man_facepalming:
Here is the current version of YAML:

esphome:
  name: generator
  friendly_name: Generator
  platformio_options:
    build_flags:
      -DBOARD_HAS_PSRAM
      -mfix-esp32-psram-cache-issue




esp32:
  board: esp-wrover-kit
  framework:
    type: arduino

# Enable logging
logger:
  logs:
    pulse_counter: INFO
# Enable Home Assistant API
api:
  reboot_timeout: 0s
  encryption:
    key: 

ota:
  password: 

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Generator Fallback Hotspot"
    password: 

captive_portal:

web_server:
  port: 80
  auth:
    username: !secret esphome_web_username
    password: !secret esphome_web_password
#--------------SENSOR--------------    
sensor:
  #### Glowplug Delay ####
  - platform: template
    name: "GP Delay"
    icon: mdi:timer-settings-outline
    id: gp_delay
    update_interval: 5s
    lambda: |-
      return id(temp).state;
    filters:
      - calibrate_polynomial:
          degree: 3
          datapoints:
          - -30 -> 30    
          - -20 -> 24    
          - 0 -> 15    
          - 20 -> 12  
          - 40 -> 10
          - 60 -> 6
          - 80 -> 0
          - 100 -> 0
          - 120 -> 0
  #### Exercise Timer ####
  - platform: template
    name: "Generator Exercise Timer"
    icon: mdi:run-fast
    id: xtime
    update_interval: 10s
  #### Water Temp ####
  - platform: adc
    pin: GPIO36
    id: temp
    unit_of_measurement: "°C"
    attenuation: auto
    name: "Gen Coolant Temp"
    icon: mdi:thermometer-water
    update_interval: 2s
    on_value_range:
      - above: 115.0
        then:
            - script.execute: problem
    filters:
      - calibrate_polynomial:
          degree: 3
          datapoints:
          - 3.047 -> -30.0    
          - 2.889 -> -20.0    
          - 2.434 -> 0.0    
          - 1.898 -> 20.0  
          - 1.417 -> 40.0
          - 1.087 -> 60.0
          - 0.875 -> 80.0
          - 0.763 -> 100.0
          - 0.703 -> 120.0
      - sliding_window_moving_average:
          window_size: 5
          send_every: 2
          send_first_at: 2
      
  #### Input Voltage ####
  - platform: adc
    pin: GPIO34
    id: volt
    unit_of_measurement: "V"
    attenuation: auto
    name: "Gen Battery Voltage"
    icon: mdi:car-battery
    update_interval: 2s
    filters:
      - calibrate_polynomial:
          degree: 2
          datapoints:
          - 0.82 -> 5.0    
          - 0.99 -> 6.0    
          - 1.155 -> 7.0    
          - 1.32 -> 8.0  
          - 1.49 -> 9.0
          - 1.66 -> 10.0
          - 1.82 -> 11.0
          - 1.99 -> 12.0
          - 2.15 -> 13.0
          - 2.33 -> 14.0
          - 2.50 -> 15.0
      - sliding_window_moving_average:
          window_size: 5
          send_every: 5
          send_first_at: 2
  #### RPM Counter ####
  - platform: pulse_counter
    name: "Gen RPM"
    update_interval: 200ms
    id: rpm
    unit_of_measurement: RPM
    accuracy_decimals: 0
    internal: true
    pin:
      number: GPIO13
      inverted: true
      mode:
        input: true
        pullup: true
    filters:
      - delta: 10
      - calibrate_linear:
          - 0.0 -> 0.0
          - 17300.0 -> 1820.0

#--------------BINARY_SENSOR--------------
binary_sensor:
  #### Air Restriction ####
  - platform: gpio
    id: "air"
    pin:
      number: GPIO21
      inverted: true
    name: "Gen Air Filter Restriction"
    icon: mdi:air-filter
  #### High Temp ####
  - platform: gpio
    id: "temp_sw"
    on_press:
      script.execute: problem
    pin:
      number: GPIO19
      inverted: true
    name: "Gen High Coolant Temp"
    icon: mdi:thermometer-alert
  #### Oil Pressure ####
  - platform: gpio
    id: "oil"
    on_press:
      script.execute: problem
    pin:
      number: GPIO18
      inverted: True
    name: "Gen Oil Pressure Low"
    icon: mdi:oil
  #### Start ####
  - platform: gpio
    id: "start"
    pin:
      number: GPIO14
      inverted: true
    name: "Gen Call for Start"
    icon: mdi:restart-alert
#--------------SWITCH--------------
switch:  
  #Override Automation
  - platform: template
    name: "Generator Override"
    icon: mdi:lock
    restore_mode: ALWAYS_OFF
    id: override
    optimistic: true

  #Startup  
  - platform: template
    name: "Generator Start"
    id: gen_start
    optimistic: true
    on_turn_on:
      - script.execute: startup
    on_turn_off:
      - script.stop: startup
      - script.stop: starter_timer
      - switch.turn_off: fuel
      - switch.turn_off: glowplug
      - switch.turn_off: starter
  #Exercise
  - platform: template
    name: "Generator Exercise"
    id: gen_exercise
    optimistic: true
    on_turn_on:
      - script.execute: exercise
    on_turn_off: 
      - script.stop: exercise
      - sensor.template.publish:
          id: xtime
          state: 0
      - switch.turn_off: gen_start
      - switch.turn_off: override

  #Glowplugs
  - platform: gpio
    inverted: false
    pin: GPIO25
    id: glowplug
    restore_mode: ALWAYS_OFF
    name: "Gen Glow Plugs"

  #Starter
  - platform: gpio
    inverted: false
    pin: GPIO32
    id: starter
    restore_mode: ALWAYS_OFF
    name: "Gen Starter"
    icon: mdi:cog-play
    on_turn_on:
      - logger.log: "Begin starter sequence."
      - script.execute: starter_timer
      - wait_until:
          sensor.in_range:
            id: rpm
            above: 750
      - logger.log: "Started."
      - switch.turn_off: starter
      - script.stop: starter_timer

  #Fuel Shutoff
  - platform: gpio
    inverted: false
    pin: GPIO33
    id: fuel
    restore_mode: ALWAYS_OFF
    name: "Gen Fuel shutoff"
    icon: mdi:fuel

  #Aux
  - platform: gpio
    inverted: false
    pin: GPIO26
    id: aux
    restore_mode: ALWAYS_OFF
    name: "Gen Aux"
#--------------SCRIPT--------------
script:
  #Start script
  - id: startup
    mode: single
    then:
    - switch.turn_on: glowplug
    - logger.log:
        format: "Running glowplugs for %.1f seconds."
        args: ['id(gp_delay).state']
    - delay: !lambda |-
                return id(gp_delay).state * 1000;
    - script.execute: glowplug_off_delay
    - switch.turn_on: fuel
    - switch.turn_on: starter
  #Exercise timer script
  - id: exercise
    mode: restart
    then:
      - logger.log: "Beginning exercise."
      - switch.turn_on: override
      - switch.turn_on: gen_start
      - sensor.template.publish:
          id: xtime
          state: 30
      - delay: 30 min
      - logger.log: "Exercise complete."
      - switch.turn_off: gen_start
      - switch.turn_off: override
      - switch.turn_off: gen_exercise
  #Starter timer to prevent cranking too long. Rests for 30 seconds and tries again.
  - id: starter_timer
    mode: restart
    then:
      - delay: 30sec
      - logger.log: "Cranked too long."
      - switch.turn_off: starter
      - switch.turn_off: fuel
      - delay: 30sec
      - switch.turn_off: gen_start
  #Runs glowplugs 5 seconds after start
  - id: glowplug_off_delay
    mode: restart
    then:
      - logger.log: "Glowplug delay starting."
      - delay: 5sec
      - switch.turn_off: glowplug
  #Timer to allow engine to cool before shutdown
  - id: cooldown
    mode: restart
    then:
      if:
        condition:
          for:
            time: 30sec
            condition:
              binary_sensor.is_off: start
        then:
          - logger.log: "Cooldown complete."
          - switch.turn_off: gen_start
    #Loop to verify there is an issue
  - id: problem
    mode: single
    then:
      if:
        condition:
          for:
            time: 10sec
            condition:
              or:
                - binary_sensor.is_on: oil
                - binary_sensor.is_on: temp_sw
                - sensor.in_range:
                    id: temp
                    above: 115.0
        then:
          - logger.log: "Problem detected! Shutting down."
          - switch.turn_off: gen_start

#--------------TIME--------------
time:
  - platform: sntp
    on_time:
      - seconds: 0
        minutes: 0
        hours: 10
        days_of_month: 1
        then:
          if:
            condition:
              for:
                time: 15sec
                condition:
                  binary_sensor.is_off: start
            then:
              - logger.log: "Commencing scheduled exercise."
              - script.execute: exercise

#--------------Interval--------------
interval: 
  #Tickdown Exercise Timer
  - interval: 1min
    then:
      if:
        condition:
          sensor.in_range:
            id: xtime
            above: 1
        then:
          sensor.template.publish:
            id: xtime
            state: !lambda 'return id(xtime).state - 1;'

  #Monitor start/stop command
  - interval: 5sec
    then:
      if:
        condition:
              for:
                time: 2sec
                condition:
                  binary_sensor.is_on: start
        then:
          if:
              condition:
                or:
                  - script.is_running: cooldown
                  - script.is_running: exercise
              then:
                - logger.log: "Call to start. Canceling cooldown/exercise"
                - script.stop: cooldown
                - switch.turn_off: override
                - script.stop: exercise
              else: 
                if:
                  condition:
                    switch.is_off: override
                  then:
                    - logger.log: "Call to start."
                    - switch.turn_on: gen_start
        else:
            if:
              condition:
                and:
                  - switch.is_off: override
                  - switch.is_on: gen_start
                  - for:
                      time: 2sec
                      condition:
                        binary_sensor.is_off: start
              then:
                - logger.log: "Call to stop."
                - script.execute: cooldown

3 Likes

Awsome, can you please list the parts you used i am thinking doing the same

For the engine, I just found a diesel engine on my local classifieds. I bought a KDW1003 out of one of the light towers that are used in construction sites. After confirming it was working (Mechanical diesel engines are dead simple. Give them fuel, and they will run).
Next, I went to https://www.centralgagenerator.com/ and ordered a powerhead that was matched to my engine. If you look up the manufacturer spec sheet for the engine they will usually list the standard/dimensions of the output shaft and housing. You can call up a rep or use the website to find the correct generator head for your engine.
The one issue I had was ordering too big of a powerhead for my engine. The powerheads will list an ideal horsepower to run them, but the engine has to put out that horsepower at 1,820 RPM, which is usually less than the peak horsepower of the engine. in my case, the peak HP is 23.7, but at 1820 RPM it is more like 12. I am currently working on a supercharger to get the engine closer to matching the powerhead, but it does work as it is (I just can’t run the HVAC and electric ovens at the same time)
Page 5 of Technical Datasheet

As for the electronics, I am using one of my Universal Controller Module v1.2 boards with 3 automotive relays in a sealed box to control the engine.
The transfer switch is a frankenstien’d kohler transfer switch (I de-soldered the microcontroller and bodged an ESP32 into it’s place, though I should just make a full replacement)

this is amazing! Im working on a generator project myself. My setup is an MEP-803 Military surplus generator, a control board made from a member of another forum, and a generac ATS. Im working on pulling data from the 8266 board on the controller. i contacted the creator of the controll board and he modified the source code to output data Via ESP-NOW. Im working on getting the data to play nicely with HA/ESP-HOME