Help with ATS diesel generator

I will try this option as well. Can someone check if I did it right? Maybe I’m missing something…

script:

  • id: my_script1
    then:
    • delay: 60s
    • switch.turn_on: switch_retea
    • delay: 300s
    • switch.turn_off: switch_start_stop_generator
    • delay: 60s
    • switch.turn_off: switch_generator
  • id: my_script2
    • switch.turn_on: switch_start_stop_generator
    • delay: 60s
    • switch.turn_off: switch_retea
    • delay: 60s
    • switch.turn_on: switch_generator
      binary_sensor:
  • platform: gpio
    pin:
    number: GPIO5
    inverted: true
    name: “Sensor Retea”
    id: sensor_retea
    on_press:
    then:
    - script.execute: my_script1
    - script.stop: my_script2
    on_release:
    then:
    - script.execute: my_script2
    - script.stop: my_script1
    switch:
  • platform: gpio
    pin: GPIO14
    name: “Switch Retea”
    id: switch_retea
    inverted: true
    interlock: switch_generator
  • platform: gpio
    pin: GPIO4
    name: “Switch Generator”
    id: switch_generator
    inverted: true
    interlock: switch_retea
  • platform: gpio
    pin: GPIO16
    name: “Switch Start/Stop Generator”
    id: switch_start_stop_generator
    inverted: true

I see a potential problem with this approach because of the embedded delays.
During those delays, the real-world conditions could change (such as utility coming back on, or going off again) and the delay just keeps going, followed by the action it precedes. The trouble being, of course, that you no longer want that action to happen.

That is the essence of my suggestion above for how the quadrant logic should look, and should rely on ESPHome’s ability to determine something like “is Utility off, and has been for at least 1 minute?”
The key to this is the “for” which is a built-in conditional keyword in ESPHome’s condition logic.

(I’m trying to find a good example of using ‘for’ in the ESPHome docs. Will update this with a link when I do)

It seems like that is how you want your generator to respond. By relying on ESPHome’s “for” condition, your logic doesn’t have to rely on delays. Every test results in either inaction, or immediate-and-complete action (e.g. stop the generator), and that action will only occur if all your criteria are met
(e.g., Utility is on, and it’s been that way for 5 minutes.)

1 Like

Ok, this is what I get for answering before coffee.
the ‘for’ test I’m thinking about is a feature in HomeAssistant, not in ESPHome. You need this to be able to operate standalone, which ESPHome can do, but it doesn’t have a built-in ‘for’ test.

So, to follow my suggestion, you’d need a timer (like the built-in millis() function) and a few Global Variables.
Whenever you make a change to a device, or detect a change in the utility (gen on, relay on, util on, etc) store the current millis() in a global related to that sensor or switch (e.g. Relay_Changed_When, Utility_Changed_When).

Then, when testing to see if the state needs to change (has the util been on for 5 minutes?) you just test whether your stored ‘when’ is more than 300000mS older than the current millis().
Now, you have the ability to tell how long the utility has been back on, so your logic knows if and when it should turn off the generator.

There’s one risky point about using the built-in millis() timer for this, and that is that the counter rolls over about every 64 days. So, if events were to occur right when it’s about to rollover, and it does, your tests for time could yield inaccurate results, once.

To avoid that, you would also want to test for how high the millis() counter is getting, and, if it’s maybe about halfway (i.e. 32 days) and if the whole system is in quiescent state (util on, gen off, it’s been so for > an hour or two), reboot itself. The reboot takes just a few seconds, but now you have another 32 days of accurate millis().
There are more elegant ways to handle counter rollover, but that adds complexity and thus the opportunity for bugs. By just rebooting every 32 days, you solve the rollover issue without extra logic, and it presumes that your generator isn’t ever likely to be in operation for > 30 days at a stretch.

I’m just at the beginning of esphome programming, I’m trying to learn. It is also a challenge, an ambition. Can you give me an example of the code, in my mind, I see how it should be, but the lack of experience in python… shows its face. As you can see, I tried several options. That’s why I turned to this forum, maybe someone more experienced can guide me in the right direction.

Happy to. Might take me a while, though. I’ve got some other tasks to take care of this morning, and then will sit down and try to compose YAML that does what I’m talking about.

Can you clarify for me what the ‘retea’ switch and sensor are? What’s ‘retea’?

‘start/stop generator’ looks like a simple binary switch: generator on, generator off, depending on the state of that [ESP] output.

I am doing something like this, you can check out my post. While I have some issues I did some of the things that you might need.

It is not a rush. The “retea switch” is the relay that controls the power main line switch, which comes from the electricity supplier.
The “retea sensor” is a relay placed on one of the electricity input phases from the supplier, it senses when there is voltage or not.
start/stop generator, is the relay which, when it is closed, starts the electricity generator, when it is open, it stops it. It is a diesel electric generator.
That’s right, a simple switch.

Thank you!

I tend to do most of my programming while doing other things. The algorithm takes shape in my head in the background.
In this case, I think two truth tables (one for the transfer relay, and one for the generator, since you don’t always want to activate them both simultaneously), and an Interval trigger.

This will therefore be a ‘polled’ control, where it tests conditions every second, and takes any appropriate action.
Ideally it would be implemented as an interrupt-driven system, but using interrupts for this can introduce subtle (not to mention surprising, even dangerous in this case) race-conditions unless you also employ mutex wrappers around the decisions to ensure they are made atomically, and that is better programmed directly in C[++].

In the simpler, 1-Second polling approach, there will be an average response delay of 0.5 Seconds (+/- 0.5S) to any change of conditions, if that’s tolerable for your application?

Polling could be done more rapidly by using a shorter interval, but if it’s made too short there becomes a risk of starving the system of ‘idle’ time during which it does housekeeping and maintains the WiFi link, etc. I chose 1S arbitrarily, there’s no math behind it, just intuition.

One second is ok, maybe more, there is no need for such precision. The idea is that when it goes from the generator to the main line, it should be fast.

Can you clarify what is the difference between:
switch_start_stop_generator
and
switch_generator
?

In addition to the above question, does this set of states accurately describe what actions you’d want taken under the varying conditions?

# State Tables
# These will tell us if we are going to be operating the system properly
# by guiding the condition-testing code we write below
# There are only 4 situations in these tables where we need to take action
# I used two separate tables in order to be sure we don't accidentally introduce
# side-effects from the combination of delays between turning the relay off versus turning the generator off.
# The logic could certainly be made more visually efficient, but that introduces risk of side-effects, and
# in this case I think you would prefer it to be provably correct, than pretty.

#### ==== Generator State Table

# UtilOn & GenOff
#   - quiescent state, take no action

# UtilOff & GenOff
#   >>> turn generator on

# UtilOn & GenOn
#   - has util been on > 5 minutes?
#     >>> turn generator off

# UtilOff & GenOn
#   - steady-state, operationally active, take no action

#### ==== Relay State Table

# UtilOn & RelayOff?
#   - quiescent state, take no action

# UtilOff & RelayOff?
#   >>> turn relay on

# UtilOn * RelayOn?
#   - has util been on > 10 seconds?
#     >>> turn relay off

# UtilOff & RelayOn?
#   - steady-state, operationally active, take no action

#### =================

Switch_start_stop_generator = the switch that starts and stops the generator engine.
Switch_generator = the switch that switches the utilities (electrical installation of the house) to the voltage from the generator

I am sending you a diagram to see logically.

I think I get it now, thanks. The name had me a bit confused.
I have logic for this in YAML that compiles, but I have yet to fully desk-check if I got it right.
Will post in a moment.

Oh, my. Two separate relays. (scared me a bit)
I had assumed the interlock was a mechanical one within the ‘retea’ device, and that it had one input signal that caused it to transfer the load from mains to generator, and it would drop back to mains when deselected.

Yes, I can see now why there are two logical ‘switches’ and why they must be exclusive of one another.

I’ll revise my YAML to reflect this.

In the next reply will be some YAML that compiles. Note: That doesn’t mean it’s correct. :wink:
(standard disclaimer applies, no warranty, etc.)

Spend some time walking through the 4 relay-generator-state tests. Be sure that for each condition it’s taking the proper actions. Remember that it’s a loop, and the series of tests will be run every second.

You’ll notice that I don’t do both relay changes in the test block where it starts the generator.
I did that on purpose. It will let the generator start in one pass, then do the load transfer (changing the relays) in the subsequent interval pass. That gives the generator about 1 second (one loop interval) to stabilize before the loop repeats and (this time) transfers the load because the generator is now running.

# There are only 4 situations in which we need to take action.
# I used separate logic blocks in order to be sure we don't accidentally introduce
# a weird combination of delays between turning the relay off versus turning the generator off.
# The logic could certainly be made more visually efficient, but that introduces risk of side-effects.

binary_sensor:

  - platform: gpio
    id: sensor_retea
    pin:
      number: GPIO5
      inverted: true
    name: “Sensor Retea”

switch:

  - platform: gpio
    id: switch_retea
    pin:
      number: GPIO14
      inverted: true
    name: “Switch Retea”
    interlock: switch_generator

  - platform: gpio
    id: switch_generator
    pin:
      number: GPIO4
      inverted: true
    name: “Switch Generator”
    interlock: switch_retea

  - platform: gpio
    id: switch_start_stop_generator
    pin:
      number: GPIO16
      inverted: true
    name: “Switch Start/Stop Generator”

globals:

   - id: sensor_retea_last_change_time
     type: long unsigned
     restore_value: no
     initial_value: '0'

   - id: sensor_retea_last_state
     type: boolean
     restore_value: no
     initial_value: 'false'

interval:
  - interval: 1s
    # There are 6 separate tests here. Their order matters.
    # the first 2 manage the load-transfer relays.
    # the next 2 manage the generator-running conrtol.
    # the next 2 tests are there to remember state and change time of mains power.
    then:

      # UtilOff, Generator running, SwitchGeneratorOff? >>> transfer load to generator
      - if:
          condition:
            and:
              - binary_sensor.is_off: sensor_retea
              - switch.is_on: switch_start_stop_generator
              - switch.is_off: switch_generator
          then:
            - switch.turn_off: switch_retea
            - switch.turn_on: switch_generator

      # UtilOn for > 5 seconds & SwitchGeneratorOn? >>> transter load to mains
        # the 5 second delay is to prevent transferring the load when 'blips' of brief power on the utility line happen while it's being restored
        # you want the utility power to be on and stable for at least a few seconds before beginning to transition load back to it.
      - if:
          condition:
            and:
              - binary_sensor.is_on: sensor_retea
              - switch.is_on: switch_generator
          then:
            - if:
                condition:
                  lambda: |-
                    // this math is immune to millis() rollover because it's all unsigned
                    return ((unsigned long)(millis() - id(sensor_retea_last_change_time)) > 5000); // (5 seconds)
                then:
                  - switch.turn_off: switch_generator
                  - switch.turn_on: switch_retea

      # UtilOff & GenOff? >>> start generator
      - if:
          condition:
            and:
              - binary_sensor.is_off: sensor_retea
              - switch.is_off: switch_start_stop_generator
          then:
            - switch.turn_on: switch_start_stop_generator

      # UtilOn for >5 minutes & GenOn? >>> turn generator off
      - if:
          condition:
            and:
              - binary_sensor.is_on: sensor_retea
              - switch.is_on: switch_start_stop_generator
          then:
            - if:
                condition:
                  lambda: |-
                    // this math is immune to millis() rollover because it's all unsigned
                    return ((unsigned long)(millis() - id(sensor_retea_last_change_time)) > 300000); // (5 minutes)
                then:
                  - switch.turn_off: switch_start_stop_generator

      # this is to track the state and timing of changes on the mains on/off sensor.
        # the purpose of this is to know how long ago the mains utility changed state
        # which is used in the relay-control logic to delay the load transfer by a few seconds
        # and to ensure that mains power has been stable for 5 minutes before stopping the generator
      - if:
          # util is on, but last_state is false
          condition:
            and:
              - binary_sensor.is_on: sensor_retea
              - lambda: |-
                  return id(sensor_retea_last_state) == false;
          then:
            - globals.set:
                id: sensor_retea_last_state
                value: 'true'
            - globals.set:
                id: sensor_retea_last_change_time
                value: !lambda |-
                        return millis();

      - if:
          # util is off, but last_state is true
          condition:
            and:
              - binary_sensor.is_off: sensor_retea
              - lambda: |-
                  return id(sensor_retea_last_state) == true;
          then:
            - globals.set:
                id: sensor_retea_last_state
                value: 'false'
            - globals.set:
                id: sensor_retea_last_change_time
                value: !lambda |-
                        return millis();

# during ESP boot, set the conditions in the global variables to the current observed conditions
# e.g. sensor_retea, and the last_change_time values to current time in millis().
esphome:
  name: generator-control
  on_boot:
    priority: 700.0
    then:
      - if:
          condition:
            - binary_sensor.is_off: sensor_retea
          then:
            - globals.set:
                id: sensor_retea_last_state
                value: 'false'
          else:
            - globals.set:
                id: sensor_retea_last_state
                value: 'true'
      - globals.set:
          id: sensor_retea_last_change_time
          value: !lambda |-
                  return millis();


esp8266:
  board: esp01_1m
logger:
  level: DEBUG
web_server:
  port: 80
  include_internal: true
captive_portal:
api:
  reboot_timeout: 0s  # default 15
ota:
wifi:
  networks:
  - ssid: your_ssid
    password: your_wifi_password