Tryng to repeat an action every "n" minutes where "n" is variable

Please bear with me because I’m a retired mechanical engineer, fast approaching my 70th birthday and struggling to keep up with this sort of tech. What I’m trying to do is (within ESPHome) turn on a GPIO at a repeatable interval where the interval is configurable via a home assistant input number. Once turned on, then the GPIO needs to stay on for a period of time depending on the value of a second home assistant input number, then turn off. I’ve created the input numbers and also created “internal” global variables which have an initial value but which also get updated when the HA input number changes. But I can’t fathom out how to configure an automation within ESPHome where both the interval and duration are based on the value of the variables rather than fixed numbers. I thought I could use “interval.interval” but it seems that the it’s not templatable and will only take fixed numbers. I think I could write a script which would turn on the GPIO, delay for “n” *1000 ms, then turn off. Might that work? If so, I’d appreciate a bit more guidance as to how to write the script. But I’m still stuck with how to set a repeatable time interval for starting that script, based on the value of a global variable. Any help would be very much appreciated.

You can use a template trigger with an input_number helper and a modulo like this

{{ (now().timestamp()
               | timestamp_custom('%M') | int) % n }}

Time trigger are documented here

You can also use this logic in esphome

In lambdas is may look like this

auto time = id(sntp_time).now();
If (time.minute%n == 0)
{ Logic here }

Thanks, but I am trying to find a solution that will run entirely on the ESP32 device. If I understand what you suggesting correctly, then if the WiFi connection failed for any reason, the automation would fail would it not? I’m not too worried about the actual time of day that the automation runs, just the period of time between.

Best practice for accurately timed events on an ESP32 - where you want it to run standalone and sometimes without internet connection, is to use a RTC.

ESPHome supports the DS1307 and the PFC85063 RTCs. Both have on-board independent batteries and keep accurate time for years (according to the specs :slight_smile: ). Both are pretty cheap. The clock will then keep the time a lot more accurately that the ESP is able to do on its own (not that well, seconds out a day).

You can add an internet time source like SNTP that will update the clock when available, you must be careful however in making sure the network connection timeout is disables so that ESPHome doesn’t reboot when no network connection.

Once you have an accurate time source use the various on_time: triggers documented in the Time Component linked above.

time:
  - platform: sntp
    # ...
    on_time:
      # Every 5 minutes
      - seconds: 0
        minutes: /5
        then:
          - switch.toggle: my_switch

      # Every morning on weekdays
      - seconds: 0
        minutes: 30
        hours: 7
        days_of_week: MON-FRI
        then:
          - light.turn_on: my_light

      # Cron syntax, trigger every 5 minutes
      - cron: '* /5 * * * *'
        then:

Your main issue is that the time interval for on_time: is not templatable. So you can’t use a value from HA to set the interval. You need to use something like Benoît’s lambda code above and check for the interval being passed in an on_time: block that runs every second. Very messy unfortunately.

Yes, not being able to template the on_time or interval depending an HA value is a bit of a pain. The use case for this is a kind of hybrid hydroponic/aeroponic system whereby a pump supplies nutrients to the plants for a period time, then turns off allowing the plant roots to absorb oxygen without drying out before the cycle repeats. To save energy costs, I want to keep the pump run-time to a minimum. So I’ll need to play around with the frequency and duration to find the minimum pump time usage without the plant roots drying out ( which might vary with things like ambient temperature etc).

But the times need not be too accurate - a few seconds here or there won’t matter much. So my next thought is maybe to use an external pulse generator set to say 1hz or thereabouts and feed the output from that into a gpio and use it to count “ticks”. Then maybe I could use this as a counter (or two) that I could compare with the HA input number sensor(s) and take the necessary action accordingly. What do you all think? Viable?

…on the other hand. After more research, and as far as I tell, “delay” can be templated and I believe the result of the return is ms. So, I’ve written a little script as follows:-

script:
  - id: cycle_pump
    then:
      - delay: !lambda return (id:pump_interval-id:pump_on_time)*1000
      - switch.turn_on: pump_relay
      - delay: !lambda return id:pump_on_time*1000
      - switch.turn_off: pump_relay 

The pump_interval and pump_on_time are global variable which are given initial values but updated when HA sensors are available or change.

So, if this works, it should delay for the interval time minus the run time in ms, then turn on the pump, then delay for the run time in ms, then turn the pump off. So the interval between one pump turn on event and the next should equal my set value.

In theory, I think I could call that script from within a while loop (the “while” being a boolean state which will enable/disable the pump cycle) and trigger it on boot or state change or some such.

Thoughts? Have I constructed the script correctly? Are there any “Gotchas”?

You can also use ESP32 with a custom firmware (Arduino IDE) and make it communicate with HA through MQTT, you can even make it discoverable.

It’s easier if you have a custom project which needs lots of logic

Thanks but that’s probably a bit too advanced for an old man like me :slightly_smiling_face:

So my next thought is maybe to use an external pulse generator set to say 1hz or thereabouts and feed the output from that into a gpio and use it to count “ticks”

That’s why RTC clock was suggested.
An RTC is basically a quartz, a battery, a register and something that count pulses (the battery is here to keep it running in case of power loss)

Also have you looked on the DIY websites if someone have done something similar or even here in Share your Projects! ?

Running a pump for gardening or for swimming pool is something frequently needed, so there’s chances that someone made something relevant to your needs

- delay: !lambda return (id:pump_interval-id:pump_on_time)*1000

I’m not sure if you’ll need to subtract the running time, because if your logic sequential the running time of the pump would already be counted on the first iterations.

Thus if interval is five minutes and running time five minutes it will run continuously since 5-5=0

It depends if you want to count from start point to start point or from stop to the next start

Hi @deckingman,
Another idea, have a look at “Slow PWM output” in ESPhome docs. I’ve never tried it myself, so can’t speak from personal experience, but it looks like it might suit your use case?
Andrew

Yes, there are different ways of looking at it. I want to be able to vary the run time independently of the total cycle time. Effectively increasing the on time will automatically reduce the off time. So the interval between cycles is from start to start. The allowable range for runtime is 60 to 300 seconds (1 to 5 minutes), and the allowable range for the cycle time is 360 to 900 seconds (6 to 15 minutes). So there will always be at least 1 minute of “off time”. I may need to adjust those parameters once the system is up and running.

Edit. Oh and I’ve spent days looking at other similar projects and ideas but nothing quite fits what I want to do.

Well I think I might be getting somewhere. I call the script on boot using a while loop but had a nasty feeling that if I change the boolean to disable the pump, then when I re-enable it, the while loop wouldn’t restart. So I’ve added while loops to all of the sensor such that if a state or value changes, it will restart. I’m sure there must be a more elegant way of achieving that. Anyway, it validates so looks like there might not be any errors - at least in my syntax. This thing also has a flow type level sensor which changes resistance as the float moves up or down, so I’ve created a couple of sensors to report the tank level as a percentage full and in litres. For the sake of completeness, the full config is below. I’d appreciate it if you knowledgeable people could take a gander to see if there are any glaringly obvious faults.

EDIT - THE FOLLOWING CODE IS FULL OF BUGS (THAT I INTRODUCED) - DO NOT USE - SEE LATER POST

esphome:
  name: hydrotower1
  friendly_name: HydroTower1
  on_boot:
    then:
      - while:
          condition:
            lambda: |-
              return id(pump_enabled).state = true
          then:
           - script.execute: cycle_pump
esp32:
  board: esp32dev
  framework:
    type: arduino
globals:
  - id: pump_enabled
    type: bool
    restore_value: yes
    initial_value: 'true'
  - id: pump_interval
    type: float
    restore_value: yes
    initial_value: '360'  
  - id: pump_on_time
    type: float
    restore_value: yes
    initial_value: '60'      
# Enable logging
logger:
# Enable Home Assistant API
api:
  encryption:
    key: "this has been redacted........."
ota:
  password: " .........as has this"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
#  manual_ip:
#    static_ip: 192.168.1.80
#    gateway: 192.168.1.1
#    subnet: 255.255.255.0
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "redacted"
    password: "redacted"

captive_portal:

switch:
  - platform: gpio
    pin: GPIO14
    #inverted: true
    name: "Pump Relay"
    id: pump_relay

binary_sensor:
  - platform: homeassistant
    name: "Hydro Pump Enable"
    entity_id: input_boolean.hydro_pump_enable
    on_state:
      then:
        - lambda: |-
            id(pump_enabled) = x;
        - while:
            condition:
              lambda: |-
                return id(pump_enabled).state = true
            then:
             - script.execute: cycle_pump
sensor:
  - platform: homeassistant
    name: Hydro Pump Interval
    entity_id: input_number.hydro_pump_interval
    on_value:
      then:
        - lambda: |-
            id(pump_interval) = x;
        - while:
            condition:
              lambda: |-
                return id(pump_enabled).state = true
            then:
             - script.execute: cycle_pump
  - platform: homeassistant
    name: Hydro Pump On Time
    entity_id: input_number.hydro_pump_on_time
    on_value:
      then:
        - lambda: |-
            id(pump_on_time) = x;
        - while:
            condition:
              lambda: |-
                return id(pump_enabled).state = true
            then:
             - script.execute: cycle_pump
  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Tank Level Raw"
    id: "tank_level_raw"
    icon: mdi:car-coolant-level
    # change this to (say) 30 secs after testing
    update_interval: 1s
    # change to true after testing
    internal: False  

  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Tank Level Percent"
    id: "tank_level_percent"
    icon: mdi:car-coolant-level
    # change this to (say) 30 secs after testing
    update_interval: 1s
    internal: False  
    # check calibration with actual voltage values
    filters:
      - calibrate_linear:
        - 0.0 -> 0
        - 0.95 -> 100
    unit_of_measurement: "%"  

  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Tank Level Litres"
    id: "tank_level_litres"
    icon: mdi:car-coolant-level
    # chnage this to say 30 secs after testing
    update_interval: 1s
    internal: False  
# max is 57 litres at end of float ravel, 62 litres to brim so say 60 lites max
# offset is 15 litres before float moves
# so with these filters, 0 volts should show 15 litres, 0.95 volts should show 45+15 = 60 litres
    filters:
      - calibrate_linear:
        - 0.0 -> 0
        - 0.95 -> 45
      - offset: 15  
    unit_of_measurement: "Litres"  

script:
  - id: cycle_pump
    then:
      - delay: !lambda return (id:pump_interval-id:pump_on_time)*1000
      - switch.turn_on: pump_relay
      - delay: !lambda return id:pump_on_time*1000
      - switch.turn_off: pump_relay

Hello fellow ME! I went down exactly that path (scripts and delays using HA inputs) while building a coffee roaster. I found a lot of the info out there completely confusing for a non-programmer such as myself and initially tried things that were WAY more complicated and unnecessary for my use-case (although maybe they would have been more elegant?). Once I started down this path, I spent most of my time troubleshooting YAML and lambda syntax issues rather than any of the actual logic, etc.

Here’s my (admittedly long) YAML file. Hopefully you can find it helpful?

substitutions:
  friendly_name: Coffee Roaster
esphome:
  name: coffee-roaster
  on_boot:
    priority: 500
    then:
      - climate.control:
          id: pid_controller
          mode: "OFF"
          target_temperature: 15.5556°C
      - text_sensor.template.publish:
          id: roast_status
          state: "On"
      - light.turn_off:
          id: neopixel_light
      - delay: 5s
      - light.turn_off:
          id: neopixel_light
      - delay: 5s
      - light.turn_off:
          id: neopixel_light

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API. Changed reboot_timeout to 0s to keep it from rebooting every 15min when using away from home.
api:
  reboot_timeout: 0s

ota:
  password: "xxxxxxxxxxxx"

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  - ssid: !secret trailer_wifi_ssid
    password: !secret wifi_password
  - ssid: !secret chucks_wifi_ssid
    password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Coffee-Roaster Fallback Hotspot"
    password: !secret wifi_password

#Removed so that when it can't connect to WiFi, the local AP it creates is for the web server and not setting up a different WiFi connection (i.e. allows the web server to be accessed without a WiFi network)
#captive_portal:
    
web_server:
  port: 80
  local: true


interval:
  - interval: 1s
    then:
    
    #Safety check to make sure that if the heater is on, the fan is also on
      - if:
          condition:
            and:            
              - lambda: 'return id(heat_value).state > 0;'
              - fan.is_off: fan_speed
          then:
            - script.stop: roast_1
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Heater On, Fan Not On"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"







    #Safety check to make sure that if the heater is 100% on for a while, the temperature actually increases.
      - if:
          condition:
            and:
              - lambda: 'return id(heat_value).state == 100;'
              - lambda: 'return id(heater_counter) == 0;' 
              - lambda: 'return id(temp_sensor).state < 100;'
          then:
            - lambda: |-
                id(heater_start_temp) = id(temp_sensor).state; 
            - lambda: |-
                id(heater_counter) = (id(heater_counter) + 1);
          else:
            - if:
                condition:
                  and:
                    - lambda: 'return id(heat_value).state == 100;'
                    - lambda: 'return id(heater_counter) > 0;'
                    - lambda: 'return id(temp_sensor).state < 100;'
                then:
                  - lambda: |-
                        id(heater_counter) = (id(heater_counter) + 1);
                else:
                  - lambda: |-
                        id(heater_counter) = 0;
                
      - if:
          condition:
            and:
              - lambda: 'return id(heater_counter) > 60;'
              - lambda: 'return id(temp_sensor).state <= id(heater_start_temp);'
              - lambda: 'return id(temp_sensor).state < 100;'
          then:
            - script.stop: roast_1
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Heater On, Temperature Not Increasing"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"
                
          else:
            - if:
                condition:
                  - lambda: 'return id(heater_counter) > 60;'
                then:
                  - lambda: |-
                      id(heater_counter) = 0;
      





number:
  - platform: template
    name: Roast 1 Preheat Temp
    optimistic: true
    id: roast_1_preheat_temp
    icon: "mdi:thermometer"
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 120
    mode: box

  - platform: template
    name: Roast 1 Temp
    optimistic: true
    id: roast_1_temp
    icon: "mdi:thermometer"
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 221
    mode: box

  - platform: template
    name: Roast 1 Ramp
    optimistic: true
    id: roast_1_ramp
    icon: "mdi:thermometer-chevron-up"
    unit_of_measurement: °C/min
    max_value: 120
    min_value: 1
    step: 1
    initial_value: 10
    mode: box

  - platform: template
    name: Roast 1 Fan Speed
    optimistic: true
    id: roast_1_fan_speed
    icon: "mdi:fan-speed-1"
    unit_of_measurement: "%"
    max_value: 100
    min_value: 1
    step: 1
    initial_value: 75
    mode: box

button:
  - platform: template
    name: "Start Roast #1"
    id: start_roast_1
    icon: "mdi:coffee"
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_1


globals:
    - id: currently_roasting
      type: int
      restore_value: no
      initial_value: '0'

    - id: current_temp_setpoint
      type: int
      restore_value: no
      initial_value: '0'
     
    #Used for heater safety interval check
    - id: heater_counter
      type: int
      restore_value: no
      initial_value: '0'
      
    - id: heater_start_temp
      type: int
      restore_value: no
      initial_value: '0'


script:

  - id: roast_timer
    mode: restart  
    then:
      - delay: 2700s
      - script.stop: roast_1
      - climate.control:
          id: pid_controller
          mode: "OFF"
      - fan.turn_on:
          id: fan_speed
          speed: 100
      - text_sensor.template.publish:
          id: roast_status
          state: "ERROR: Roast Timeout"
      - light.turn_on:
          id: neopixel_light
          brightness: 100%
          red: 100%
          green: 0%
          blue: 0%
          effect: "Blink"


  - id: roast_1
    
    then:
      - globals.set:
          id: currently_roasting
          value: '1'

      - light.turn_on:
          id: button_light_switch_1
        
      - light.turn_off:
          id: button_light_switch_2
        
      - light.turn_off:
          id: button_light_switch_3
        
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 100%
          blue: 0%
        
      - fan.turn_on:
          id: fan_speed
          speed: !lambda |-
                                return id(roast_1_fan_speed).state;
        
      - delay: 5s
      
      - climate.control:
         id: pid_controller
         mode: HEAT
         target_temperature: !lambda |-
                                return id(roast_1_preheat_temp).state;
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Preheat"
        
      - wait_until:
          condition:
            lambda: |-
              return id(temp_sensor).state > (id(roast_1_preheat_temp).state - 1);

          timeout: 300s
            
      - if:
          condition:
            - lambda: |-
                return id(temp_sensor).state < (id(roast_1_preheat_temp).state - 1);
                  
          then:
            - script.stop: roast_1
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Preheat Timeout"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"
                  
          else:      
      
      - delay: 120s
        
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 0%
          blue: 0%
        
      - globals.set:
                  id: current_temp_setpoint
                  value: !lambda |-
                            return id(roast_1_preheat_temp).state;
      
      - script.execute: roast_timer
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Ramp-up"
      
      - while:
          condition:
            or:
              - lambda: |-
                  return id(temp_sensor).state < id(roast_1_temp).state;

#Was having issues with the roast ending early but not throwing errors. Changed from AND to OR above and added this to try at catch a situation where the temp sensor doesn't read a number (i.e. so it ISN'T less than the roast temp).              
              - lambda: |-  
                  return isnan(id(temp_sensor).state);
            
          then:
                
              - climate.control:
                  id: pid_controller
                  mode: HEAT
                  target_temperature: !lambda |-
                      return id(current_temp_setpoint);
                
              # delay will be calculated by dividing 60 by C/min value
              - delay: !lambda |-
                            return 60000 / id(roast_1_ramp).state;
                
              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) < id(roast_1_temp).state;
                  
                  then:
                    - lambda: |-
                        id(current_temp_setpoint) = (id(current_temp_setpoint) + 1);
                  
                  else:
      
      - script.stop: roast_timer
      
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 65%
          blue: 0%
        
      - climate.control:
          id: pid_controller
          mode: "OFF"
          target_temperature: 15.5556°C
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Cooldown"
      
      - wait_until:
          condition:
              sensor.in_range:
                  id: temp_sensor
                  below: 40
          timeout: 1200s
      
      - delay: 60s
        
      - fan.turn_off: fan_speed

      - globals.set:
          id: currently_roasting
          value: '0'
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Done"
      
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 0%
          green: 100%
          blue: 0%

  - id: blank_script
    
    then:


light:

  - platform: binary
    name: $friendly_name Button Light 1
    output: button_light_1
    id: button_light_switch_1
    restore_mode: ALWAYS_OFF

  - platform: binary
    name: $friendly_name Button Light 2
    output: button_light_2
    id: button_light_switch_2
    restore_mode: ALWAYS_OFF

  - platform: binary
    name: $friendly_name Button Light 3
    output: button_light_3   
    id: button_light_switch_3
    restore_mode: ALWAYS_OFF
    
  - platform: neopixelbus
    type: RGB
    variant: SK6812
    pin: GPIO22
    num_leds: 1
    name: $friendly_name NeoPixel
    id: neopixel_light
    restore_mode: ALWAYS_OFF
    default_transition_length: 0s
    effects:
      - pulse:
          name: "Blink"
          transition_length: 0s
          update_interval: 1s


binary_sensor:
  - platform: gpio
    pin:
        number: GPIO27
        inverted: true
        mode:
            input: true
            pullup: true
    name: $friendly_name Button 1
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_1

  - platform: gpio
    pin:
        number: GPIO32
        inverted: true
        mode:
            input: true
            pullup: true
    name: $friendly_name Button 2

  - platform: gpio
    pin:
        number: GPIO33
        inverted: true
        mode:
            input: true
            pullup: true
    name: $friendly_name Button 3


output:


  - id: button_light_1
    platform: gpio
    pin: GPIO4

  - id: button_light_2
    platform: gpio
    pin: GPIO25
    
  - id: button_light_3
    platform: gpio
    pin: GPIO26


  - platform: ledc
    pin: GPIO16
    frequency: 1000 Hz #might need to change to get it to work?
    id: fan_pwm
    min_power: 0.2
    zero_means_zero: true

  - platform: slow_pwm
    pin: GPIO17
    id: heater_pwm
    period: 15s
    turn_on_action:
      - fan.turn_on:
          id: fan_speed


    
    #Required this action and leaving it blank threw errors, so I just made a script to call that does nothing.
    turn_off_action:
      - script.execute: blank_script
    

fan:
  - platform: speed
    output: fan_pwm
    name: $friendly_name Fan
    id: fan_speed
    restore_mode: ALWAYS_OFF

#  - platform: speed
#    output: heater_pwm
#    name: $friendly_name Heater
#    icon: mdi:heating-coil
#    restore_mode: ALWAYS_OFF

climate:
  - platform: pid
    name: $friendly_name PID Controller
    id: pid_controller
    sensor: temp_sensor
    default_target_temperature: 15.5556°C
    heat_output: heater_pwm
    control_parameters:
      kp: 0.07276 #0.05009
      ki: 0.00364 #0.00230
      kd: 0.36376 #0.27302
    visual:
      min_temperature: 15.5556°C
      max_temperature: 270°C
      temperature_step: 1°C
    

switch:
  - platform: template
    name: $friendly_name PID Controller Autotune
    turn_on_action:
      - climate.pid.autotune: pid_controller

spi:
  clk_pin: GPIO14 #14 #18
  mosi_pin: GPIO13 #13 #23
  miso_pin: GPIO12 #12 #19

sensor:
  - platform: max31855
    name: $friendly_name Temperature
    cs_pin: GPIO15 #15 #5
    update_interval: 1s
    #filters:
    #  - lambda: return x * (9.0/5.0) + 32.0;
    #unit_of_measurement: "°F"
    id: temp_sensor
    on_value_range:
      - above: 280
        then:
          - script.stop: roast_1
          - climate.control:
              id: pid_controller
              mode: "OFF"
          - fan.turn_on:
              id: fan_speed
              speed: 100
          - text_sensor.template.publish:
              id: roast_status
              state: "ERROR: OVERHEAT PROTECTION"
          - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"

  - platform: wifi_signal
    name: $friendly_name WiFi Strength
    update_interval: 60s
  - platform: uptime
    name: $friendly_name Uptime

  - platform: pid
    name: $friendly_name PID Kp
    type: KP
  - platform: pid
    name: $friendly_name PID Ki
    type: KI
  - platform: pid
    name: $friendly_name PID Kd
    type: KD
  - platform: pid
    name: $friendly_name PID Heat
    type: HEAT
    id: heat_value


text_sensor:
  - platform: wifi_info
    ip_address:
      name: $friendly_name IP Address
  
  - platform: template
    name: $friendly_name Status
    id: roast_status
    icon: "mdi:card-text"
    

status_led:
  pin:
    number: GPIO2
    inverted: false

There’s a lot in there, but particularly relevant to you: I setup variables adjustable within HA that are then used in a long script I call whenever I start the roasting process.

1 Like

As a follow up to this and if anyone stumbles on this thread in the future, the previous code that I posted is full of bugs and errors that I made, which cause compilation to fail. If there those errors are rectified, further bugs cause stack over flow errors and all sorts of other nasty things. I’ve edited the post containing that erroneous code to make that clear.;

But after a great deal of research and reading, I’ve ironed all that out and now have a working solution to repeat a actions every “n” minutes where “n” is variable. The variable(s) being global variables which can be altered via input numbers in HA. In summary, the actions are contained in a script and the script uses templated delays which use the global variables. To get the script to repeat constantly, I use “on_loop” which, according to the docs runs every 16ms or so. This on_loop checks if the script is running, and if it isn’t, it runs it (I discovered that repeatedly calling a script which is already running results in all sorts of nasty things happening).

Things to note (which I didn’t know until now) are that the delay only applies to the action(s) which immediately follow it. That is to say, it’s “non-blocking” as far as other things are concerned and any changes to sensor values etc will continue to happen and be published while the delay is running. Because of the way the “loop” runs, there will be a 16ms delay between each new execution of the script. In my case, the nearest minute or so will suffice so it isn’t an issue.

For the sake of completeness here is the fully working code that I used and which has been running flawlessly for 12+ hrs as I write this. I’ll mark this post as the solution to my OP. (The code also includes various adc sensors and an “enable” boolean which are not relevant to this topic).

esphome:
  name: hydrotower1
  friendly_name: HydroTower1
  on_loop:
    then:
      - if:
          condition:
            not:
              script.is_running: cycle_pump
          then:
            - script.execute: cycle_pump
esp32:
  board: esp32dev
  framework:
    type: arduino
globals:
  - id: pump_enabled
    type: bool
    restore_value: yes
    initial_value: 'true'

  - id: pump_interval
    type: int
    restore_value: yes
    initial_value: '360'  

  - id: pump_on_time
    type: int
    restore_value: yes
    initial_value: '60'      

# 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: "Hydrotower1 Fallback Hotspot"
    password: "*********"
captive_portal:

switch:
  - platform: gpio
    pin: GPIO14
    inverted: true
    name: "Pump Relay"
    id: pump_relay

binary_sensor:
  - platform: homeassistant
    name: "Hydro Pump Enable"
    entity_id: input_boolean.hydro_pump_enable
    on_state:
      then:
        - lambda: |-
            id(pump_enabled) = x;

sensor:
  - platform: homeassistant
    name: Hydro Pump Interval
    entity_id: input_number.hydro_pump_interval
    on_value:
      then:
        - lambda: |-
            id(pump_interval) = x;

  - platform: homeassistant
    name: Hydro Pump On Time
    entity_id: input_number.hydro_pump_on_time
    on_value:
      then:
        - lambda: |-
            id(pump_on_time) = x;

  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Tank Level Raw"
    id: "tank_level_raw"
    icon: mdi:car-coolant-level
    update_interval: 60s
    # change to true after testing
    internal: False  

  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Tank Level Percent"
    id: "tank_level_percent"
    icon: mdi:car-coolant-level
    update_interval: 60s
    internal: False  
    filters:
      - calibrate_linear:
        - 0.08 -> 0
        - 1.52 -> 100
    unit_of_measurement: "%"  

  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Tank Level Litres"
    id: "tank_level_litres"
    icon: mdi:car-coolant-level
    update_interval: 60s
    internal: False  
# max is 57 litres at end of float ravel, 62 litres to brim so say 60 lites max
# offset is 15 litres before float moves
# so with these filters, 0 volts should show 15 litres, 0.95 volts should show 45+15 = 60 litres
    filters:
      - calibrate_linear:
        - 0.08 -> 0
        - 1.52 -> 45
      - offset: 15  
    unit_of_measurement: "Litres"  

script:
  - id: cycle_pump
    then:
      - if:
          condition:
            lambda: |-
               return id(pump_enabled) = true;
          then:
              - logger.log: "Started First (Interval-run time) Delay"
              - delay: !lambda |-
                  return (id(pump_interval) - id(pump_on_time)) * 1000;
              - switch.turn_on: pump_relay
              - logger.log: "Started second (run time) Delay"
              - delay: !lambda |-
                  return id(pump_on_time) * 1000;
              - switch.turn_off: pump_relay
3 Likes

Thank you <3
This thread helped me a lot.
How did you get them inside HA to adjust the values?

I created number helpers. Those are the entities referred to above as platform homeassistant with the ids “input_number.hydro_pump_interval” and “input_number.hydro_pump_on_time”.