Dynamic automation triggers

I have a sensor.start_times variable which contains a string like: 11:00:30, 14:20:11, 16:12:10. These are the times in a day when I want to switch on a relay for a previously configured duration. The variable can contain 1 from 10 exact times so it can change.

Is it possible to change the triggers of an automation when I push a button or something? Or do you have any other idea how to implement this logic? I was suggested to create an automation which is triggered in every second to check if the current time is equal to one of my times but that just sounds a waste of resource.

Interesting problem. Do you really need precision down to the second? I don’t think that is possible using normal automation triggers. A time trigger only supports limited templates, so it cannot query the state of an entity. While you can use a template trigger that compares the times in the sensor against now(), that would only run once at the start of each minute.

So what can you do instead? The first thought that pops into my head would be to put the trigger in the actions of the automation. Put the next execution time into a variable and use a time trigger, which ought to work even with only limited templates? Put the automation in mode restart along with a state trigger that reacts to any changes in the sensor, as well as when HASS starts or the automation is enabled.

Something like below, just add your automation actions at the bottom. Completely untested, probably has typos or something I haven’t considered, caveat emptor. If the time trigger does not work (due to template limitations), the delay alternative definitely should though.

mode: restart
triggers:
  - trigger: state
    entity_id: sensor.start_times
    to: ~
  - trigger: event
    event_type: automation_reloaded
  - trigger: template
    value_template: "{{ is_state(this.entity_id, 'on') }}"
variables:
  next_trigger: >-
    {{ states('sensor.start_times').split(',')
    | map('today_at') | select('>', now()) | sort | first }}
actions:
  - wait_for_trigger:
      - trigger: time
        at: "{{ next_trigger.time() }}"
# - delay: "{{ (next_trigger - now()).total_seconds() }}"
# YOUR ACTIONS HERE

Probably someone else will chime in and suggest that you instead pair your existing sensor with a binary_sensor that turns true when the automation should fire. I haven’t really worked with template entities at all though, so the above is what I’d go with.

What is the integration that created sensor.start_times?

For example, is it a Template Sensor created by the Template integration? Or an MQTT Sensor?

My underlying question is how much control do you have over the configuration of sensor.start_times?

Write an automation that triggers on the state of your sensor that has the times in it. (So when the list of times changes, this automation runs.)

In the automation, parse the list of times and figure out the one that is next. Calculate the time difference between now and that next time. Then set a timer for that amount of time.

Then use the timer as the trigger for your dynamic automation.

In this approach, you always have a timer running. But the automations don’t need to fire every second.

@zbertalan - I think this question from 123 is critical. It would give us a chance to help you.

Maybe post your automation in full?

Also, what do you try to achieve? Could you elaborate your setup / your sensors / the automation / background / what are you trying to do at the end? I’m thinking maybe there are other ways to achieve the same, without comparing timestamps.

What I want to achive is:

I’d like to run my water pump every day with these specs:

  • input_datetime.pump1_start_time: it has only the time part like 16:00. This is the time in a day when irrogation is enabled from.
  • input_datetime.pump1_end_time: it also has only the time part like: 20:00. This is the time in a day when irrogation is enabled to.
  • input_number.pump1_cycles: int type, like 5. This shows that how many irrogation cycles should happen beetween the start and end time
  • input_number.pump1_duration: int type, like 30 (in seconds). This is the ON duration time of the relay
  • input_button.pump1_save_settings: a trigger to calculate the new start times of every cycle

I have an esp32 connected to the system which controls an 8 channel relay board.

I have these files now:

configuration.yaml

input_button:
  pump1_save_settings:
    name: "Save pump1 settings"
    icon: mdi:content-save

input_number:
    pump1_cycles:
    name: "Pump1 cycles"
    min: 1
    max: 10
    step: 1
  pump1_duration:
    name: "Pump1 working time [s]"
    min: 10
    max: 100
    step: 5

input_datetime:
  pump1_start_time:
    name: "Pump1 start time"
    has_time: true
  pump1_end_time:
    name: "Pump1 end time"
    has_time: true

template:
  - sensor:
      - name: "Pump 1 Cycle Start Times"
        state: "{{ states('sensor.pump1_cycle_start_times') }}"
        icon: mdi:timer

automation.yaml

- alias: Calculate Pump 1 Cycle Start Times
  triggers:
  - trigger: state
    entity_id:
    - input_button.pump1_save_settings
  - trigger: homeassistant
    event: start
  action:
  - service: python_script.calculate_cycle_start_times
    data:
      sensor_name: sensor.pump1_cycle_start_times
      start_time: '{{ states(''input_datetime.pump1_start_time'') }}'
      end_time: '{{ states(''input_datetime.pump1_end_time'') }}'
      cycles: '{{ states(''input_number.pump1_cycles'') | int }}'
      duration: '{{ states(''input_number.pump1_duration'') | int }}'

And this is the python script to calculate the times:

start_hour, start_minute = int(data['start_time'].split(":")[0]), int(data['start_time'].split(":")[1])
end_hour, end_minute = int(data['end_time'].split(":")[0]), int(data['end_time'].split(":")[1])

start_seconds = start_hour * 3600 + start_minute * 60
end_seconds = end_hour * 3600 + end_minute * 60

final_cycle_start_time_seconds = end_seconds - data["duration"]

cycle_start_times = []

if data["cycles"] == 1:
    cycle_start_time = start_seconds
    cycle_hour = int(cycle_start_time // 3600)
    cycle_minute = int((cycle_start_time % 3600) // 60)
    cycle_second = int(cycle_start_time % 60)
    cycle_start_times.append(f"{cycle_hour:02}:{cycle_minute:02}:{cycle_second:02}")
    print(cycle_start_times)
elif data["cycles"] == 2:
    curr_times = [start_seconds, final_cycle_start_time_seconds]
    for time in curr_times:
        cycle_hour = int(time // 3600)
        cycle_minute = int((time % 3600) // 60)
        cycle_second = int(time % 60)
        cycle_start_times.append(f"{cycle_hour:02}:{cycle_minute:02}:{cycle_second:02}")
    print(cycle_start_times)
else:
    cycle_interval = (final_cycle_start_time_seconds - start_seconds) / (data["cycles"] - 1)
    for i in range(data["cycles"]):
        cycle_start_time = start_seconds + (cycle_interval * i)
        print(cycle_start_time)
        cycle_hour = int(cycle_start_time // 3600)
        cycle_minute = int((cycle_start_time % 3600) // 60)
        cycle_second = int(cycle_start_time % 60)
        cycle_start_times.append(f"{cycle_hour:02}:{cycle_minute:02}:{cycle_second:02}")

cycle_start_times_str = ", ".join(cycle_start_times)

hass.states.set(data['sensor_name'], cycle_start_times_str)

I was suggested a few irrogation control add-on but none of them is what I need right now.

trigger: template
value_template: |
  {% set times_list = (states('sensor.start_times')).split(',') | map('today_at') | list %}
  {{ now() > (times_list|select('>',now()-timedelta(minutes=1))|sort|first).replace(second=0) }}

Keep in mind that the use of now() means the template will be rendered at the top of each minute.

Optionally, (if you think you might need it elsewhere) you could just put the above template in a Template binary sensor, then use the sensor’s state in a State trigger.

Thanks for the help everybody. I chose the timer option.

Solution:

configuration.yaml

input_boolean:
  system_auto_mode:
    name: "Manual/Auto mode"
    icon: mdi:autorenew

input_button:
  pump1_save_settings:
    name: "Save pump1 settings"
    icon: mdi:content-save

input_number:
    pump1_cycles:
    name: "Pump1 cycles"
    min: 1
    max: 10
    step: 1
  pump1_duration:
    name: "Pump1 working time [s]"
    min: 10
    max: 100
    step: 5

input_datetime:
  pump1_start_time:
    name: "Pump1 start time"
    has_time: true
  pump1_end_time:
    name: "Pump1 end time"
    has_time: true

timer:
  pump1_next_cycle:
    duration: "00:00:30"

template:
  - sensor:
      - name: "Pump 1 Cycle Start Times"
        state: "{{ states('sensor.pump1_cycle_start_times') }}"
        icon: mdi:timer

automations.yaml

- alias: Calculate Pump 1 Cycle Start Times
  triggers:
  - trigger: state
    entity_id:
    - input_button.pump1_save_settings
  - trigger: homeassistant
    event: start
  action:
  - service: python_script.calculate_cycle_start_times
    data:
      sensor_name: sensor.pump1_cycle_start_times
      start_time: '{{ states(''input_datetime.pump1_start_time'') }}'
      end_time: '{{ states(''input_datetime.pump1_end_time'') }}'
      cycles: '{{ states(''input_number.pump1_cycles'') | int }}'
      duration: '{{ states(''input_number.pump1_duration'') | int }}'

- alias: "Schedule Next Pump1 Cycle"
  trigger:
    - platform: state
      entity_id: sensor.pump1_cycle_start_times
    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.pump1_next_cycle
  action:
    - delay: "00:00:01"
    - service: python_script.schedule_next_cycle
      data:
        start_times: '{{ states(''sensor.pump1_cycle_start_times'') }}'
        timer_name: timer.pump1_next_cycle

- alias: "Trigger Pump1 on Timer Finish"
  trigger:
    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.pump1_next_cycle
  condition:
    - condition: state
      entity_id: input_boolean.system_auto_mode
      state: "on"
  action:
    - service: switch.turn_on
      entity_id: switch.fodder_relay_1
    - delay: 
        seconds: "{{ states('input_number.pump1_duration') | int }}"
    - service: switch.turn_off
      entity_id: switch.fodder_relay_1

calculate_cycle_start_times.py

start_hour, start_minute = int(data['start_time'].split(":")[0]), int(data['start_time'].split(":")[1])
end_hour, end_minute = int(data['end_time'].split(":")[0]), int(data['end_time'].split(":")[1])

start_seconds = start_hour * 3600 + start_minute * 60
end_seconds = end_hour * 3600 + end_minute * 60

final_cycle_start_time_seconds = end_seconds - data["duration"]

cycle_start_times = []

if data["cycles"] == 1:
    cycle_start_time = start_seconds
    cycle_hour = int(cycle_start_time // 3600)
    cycle_minute = int((cycle_start_time % 3600) // 60)
    cycle_second = int(cycle_start_time % 60)
    cycle_start_times.append(f"{cycle_hour:02}:{cycle_minute:02}:{cycle_second:02}")
elif data["cycles"] == 2:
    curr_times = [start_seconds, final_cycle_start_time_seconds]
    for time in curr_times:
        cycle_hour = int(time // 3600)
        cycle_minute = int((time % 3600) // 60)
        cycle_second = int(time % 60)
        cycle_start_times.append(f"{cycle_hour:02}:{cycle_minute:02}:{cycle_second:02}")
else:
    cycle_interval = (final_cycle_start_time_seconds - start_seconds) / (data["cycles"] - 1)
    for i in range(data["cycles"]):
        cycle_start_time = start_seconds + (cycle_interval * i)
        cycle_hour = int(cycle_start_time // 3600)
        cycle_minute = int((cycle_start_time % 3600) // 60)
        cycle_second = int(cycle_start_time % 60)
        cycle_start_times.append(f"{cycle_hour:02}:{cycle_minute:02}:{cycle_second:02}")

cycle_start_times_str = ", ".join(cycle_start_times)

hass.states.set(data['sensor_name'], cycle_start_times_str)

schedule_next_cycle.py

cycle_times = data['start_times'].split(",")
now = datetime.datetime.now().time()

next_cycle = None
for cycle in cycle_times:
    cycle_time = datetime.datetime.strptime(cycle.strip(), "%H:%M:%S").time()
    if cycle_time > now:
        next_cycle = cycle_time
        break

if not next_cycle:
    next_cycle = datetime.datetime.strptime(cycle_times[0].strip(), "%H:%M:%S").time()
    tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
    next_cycle_dt = datetime.datetime.combine(tomorrow, next_cycle)
else:
    next_cycle_dt = datetime.datetime.combine(datetime.datetime.now(), next_cycle)

now_dt = datetime.datetime.combine(datetime.datetime.now(), now)
time_difference = (next_cycle_dt - now_dt).seconds

hass.services.call("timer", "start", {
    "entity_id": data['timer_name'],
    "duration": time_difference
})