Script sequence with condition state of 1 switch

A few things.

First, you do not need to “turn_off” script.easyplus_state. Since it has no delay/wait statements, it will never turn on.

Secondly, each script is a separate thing. And each “service” in the “sequence” of a script is executed by a call (which means Home Assistant asks it to run, and then moves on, without waiting for it to finish). So, this explains why switch.turn_on is running before script.easyplus_state has finished.

You can achieve your goal in a few ways:

  1. move the “delay” into dishwasher_turn_on. Use a template for the delay time. Something like…
delay:
  seconds: >-
    {% if is_state('switch.easyplus', 'off') %}
      17
    {% else %}
      0
    {% endif %}

To make this option work, you’ll likely need to add a delay to the top of script.easyplus_turn_on so that the switch is still “off” when this part of the sequence is evaluated (since it’s happens after your request to turn it on). You may also need to increase the delay a bit from 17 in order to give enough time for things to happen.

  1. You can use a wait_template and a template binary_sensor to ensure that the switch has been on for 17 seconds and get rid of the delay in script.easyplus_turn_on entirely.

First you’ll need a binary_sensor.template:

binary_sensor:
  - platform: template
    sensors:
      easyplus_on_17s:
        value_template: "{{ is_state('switch.easyplus','on') }}"
        delay_on:
          seconds: 17

Then modify your automation to look like this:

  dishwasher_turn_on:
    sequence:
    - service: script.easyplus_state 
    - wait_template: "{{ is_state('binary_sensor.easyplus_on_17s', 'on') }}"
      timeout:
        seconds: 22 #give it 5 extra seconds in case of delays
      continue_on_timeout: false
    - service: switch.turn_on
      entity_id: switch.stp_kitchen_equipment
    - service: notify.merwone
      data_template:
        message: '
        3 dishwasher {{states.switch.stp_kitchen_equipment.state}}'

Personally, I would go with option 2. Additionally, I would get rid of script.easyplus_state and script.noop entirely and just add a condition to the top of script.easyplus_turn_on. Like:

 easyplus_turn_on:
    sequence:
    - condition: state
      entity_id: switch.easyplus
      state: "off"
    - service: notify.merwone
      data_template:
        message: "1 easyplus is {{states.switch.easyplus.state}} - turning on"
    - service: switch.turn_on
      entity_id: switch.easyplus

The logic would be as follows…

Your automations and such will only ever call script.dishwasher_turn_on

The first thing the script does it turn on the easyplus using script.easyplus_turn_on. This script only turns it on if it’s not already on. If your device isn’t changed in any way by turning it on again, you could replace the call to this script with a simple switch.turn_on service call instead.

Next, it waits for the switch to be on for 17s (as helped by the binary_sensor template).

Finally, it turns on stp_kitchen_equipment.

1 Like

@swiftlyfalling thanks for your extensive answer - checking it out…
I do like option 2, however one additional request:

Almost all my scripts, like dishwasher_turn_on, will depend on state binary_sensor.easyplus_on_17s: is it possible to introduce wait template at a higher level so I don’t need to refer to wait_template in all the scenes I setup?

Thanks again.

In the meantime I have tested option 2.

In notifications now I receive:

image

For completeness sake my complete configuration for this rf switch below.

binary_sensor:

  #rf switch 2
  - platform: mqtt
    name: 'rf switch 2'
    state_topic: 'home/switch_2'
    off_delay: 1
    device_class: plug

input_boolean:

  dishwasher:
    name: dishwasher
    initial: off
    icon: mdi:washer

binary_sensor:

  - platform: template
    sensors:
      easyplus_on_17s:
        value_template: "{{ is_state('switch.easyplus','on') }}"
        delay_on:
          seconds: 17

script:

  #turn on easyplus script if off
  easyplus_turn_on:
    sequence:
    - condition: state
      entity_id: switch.easyplus
      state: "off"
    - service: notify.merwone
      data_template:
        message: "1 easyplus is {{states.switch.easyplus.state}} - turning on"
    - service: switch.turn_on
      entity_id: switch.easyplus

  #dishwasher turn on
  dishwasher_turn_on:
    sequence:
    - service: script.easyplus_turn_on 
    - wait_template: "{{ is_state('binary_sensor.easyplus_on_17s', 'on') }}"
      timeout: '00:00:20'
      continue_on_timeout: false
    - service: switch.turn_on
      entity_id: switch.stp_kitchen_equipment
    - service: notify.merwone
      data_template:
        message: '
        3 dishwasher {{states.switch.stp_kitchen_equipment.state}}'

automation:

  - id: rf_011
    initial_state: 'true'
    alias: '[rf] switch 2'
    trigger:
    - platform: state
      entity_id: binary_sensor.rf_switch_2
      from: 'off'
      to: 'on'
    action:
    - service: homeassistant.toggle
      entity_id: input_boolean.dishwasher
    - service: notify.merwone
      data_template:
        message: '{{ trigger.to_state.name }} toggled dishwasher'

##dishwasher
  #dishwasher start
  - id: dishwasher_01
    initial_state: 'true'
    alias: dishwasher start
    trigger:
      platform: state
      entity_id: input_boolean.dishwasher
      from: 'off'
      to: 'on'
    action:
      - service: script.dishwasher_turn_on
      - delay:
        # Supports milliseconds, seconds, minutes, hours, days
          seconds: 1
      - service: notify.merwone
        data_template:
          message: '
          4 dishwasher program started:
          
          easyplus {{states.switch.easyplus.state}}

          kitchen equipment {{states.switch.stp_kitchen_equipment.state}}'
  #dishwasher stop manual
  - id: dishwasher_02
    initial_state: 'true'
    alias: dishwasher stop manual
    trigger:
      platform: state
      entity_id: input_boolean.dishwasher
      from: 'on'
      to: 'off'
    action:
    - delay:
      # Supports milliseconds, seconds, minutes, hours, days
        seconds: 30
    - service: homeassistant.turn_off
      entity_id: switch.stp_kitchen_equipment
    - delay:
      # Supports milliseconds, seconds, minutes, hours, days
        seconds: 1 
    - service: notify.merwone
      data_template:
        message: '
        dishwasher program completed:
        
        easyplus {{states.switch.easyplus.state}}

        kitchen equipment {{states.switch.stp_kitchen_equipment.state}}'
  #dishwasher stop automated
  - id: dishwasher_04
    initial_state: 'true'
    alias: dishwasher stop automated
    trigger:
      platform: state
      entity_id: input_boolean.dishwasher
      from: 'off'
      to: 'on'
      for:
        minutes: 35
        seconds: 0
    action:
    - service: homeassistant.turn_off
      entity_id: input_boolean.dishwasher, switch.stp_kitchen_equipment
    - delay:
      # Supports milliseconds, seconds, minutes, hours, days
        seconds: 1 
    - service: notify.merwone
      data_template:
        message: '
        dishwasher program completed:
        
        easyplus {{states.switch.easyplus.state}}

        kitchen equipment {{states.switch.stp_kitchen_equipment.state}}'

To my knowledge, there’s no way to have any item in a sequence cause a delay in the next item aside from wait_template and delay.

Now that I see the whole picture though, you may be interested in switch.template (https://www.home-assistant.io/components/switch.template/).

The action sequences in those, though, will likely still need to turn on switch.easyplus and then wait for easyplus_on_17s before proceeding.

I guess you could do something like this:

easyplus_then_service_call:
  sequence:
    - service: script.easyplus_turn_on
    - wait_template: "{{ is_state('binary_sensor.easyplus_on_17s', 'on') }}"
      timeout: '00:00:20'
      continue_on_timeout: false
    - service_template: "{{ service }}"

but that will only work if the “rest” of the sequence is in another script. For your dishwasher:

dishwasher_turn_on:
  sequence:
    - service: script.easyplus_then_service_call
      data:
        service: script.dishwasher_turn_on_part2


dishwasher_turn_on_part2:
  sequence:
    - service: switch.turn_on
      entity_id: switch.stp_kitchen_equipment
    - service: notify.merwone
      data_template:
        message: '
        3 dishwasher {{states.switch.stp_kitchen_equipment.state}}'

To me, this is a lot less readable.

Can you do me a favor and describe, in words not scripts, what you are attempting to achieve?

@123 off course. I have put a sonoff rf switch (switch.easyplus) between a proprietary home automation server (easyplus) and my power utility cabinet. During the day, when I am away, … it is switched off by default. So all my lights and the ‘triggerable’ switches can not be turned on without turning on switch.easyplus. Easyplus can only be reached 17" (telnet) after switch.easyplus is turned on.

Several scenes (automation/scripts) depend on the switch.easyplus to work. In order to avoid to create 2 or even more automations/scripts for each scene with condition switch.easyplus on/off I would like to make my code much lighter by adding a default check in the middle (condition). The quick and dirty solution of adding turn on switch.easyplus with delay of 17" is not acceptable.

So bottomline: I would like to have lighter and more maintenance friendly scenes environment. The more centralised this can be set up the better. That’s why I was hoping to have wait template at script.easyplus_turn_on level in option 2 example above.

Hope this makes sense.

@swiftlyfalling thanks a lot for your help.

Eventually got it working consistently with your option 2, small changes though to reflect my rock64 performance running hass in docker environment. Not sure whether relevant.

Extended delay/timeout binary sensor and script easyplus to 20" both with wait template timeout set to ‘true’ and adding to script a 1" delay between homeassistant.turn_on and notify service.

Not sure what made the difference, probably 20" and true.

How about using a single python_script, called easyplus, to serve as a ‘middleman’ for all your scripts?

To run a script, instead of running it directly, like this:

  action:
    - service: script.dishwasher_turn_on

you would call python_script.easyplus and give it the script’s name:

  action:
    - service: python_script.easyplus
      data:
        script_name: dishwasher_turn_on

The easyplus python_script serves as a ‘middleman’ and ensures switch.easyplus is on before executing the supplied script. If the switch is off, it turns it on and then pauses for 17 seconds before running the supplied script.

The idea is that none of your scripts need to be concerned with managing the state of switch.easyplus because that’s the responsibility of python_script.easyplus.

Here’s the python_script. It includes error-checking to ensure:

  • it receives a script_name
  • the entity switch.easyplus exists
  • the supplied script exists

easyplus.py

sn = data.get('script_name')
ep = hass.states.get('switch.easyplus')

if sn is not None:
  ss = hass.states.get('script.' + sn)
  if ss is not None:
    if ep is not None:
      if ep.state == 'off':
        service_data = {'entity_id':'switch.easyplus'}
        hass.services.call('switch', 'turn_on', service_data, False)    
        time.sleep(17)

      hass.services.call('script', sn, '', False)
    else:
      logger.warning('<easyplus> switch.easyplus does not exist.')
  else:
    logger.warning('<easyplus> Supplied script does not exist.')
else:
  logger.warning('<easyplus> No script name was supplied.')

If you like this idea, I can show you how to modify it so that it can also run scenes.


To use python_scripts in Home Assistant, you need to add this line to configuration.yaml:
python_script:

Create a new sub-directory in your config directory:

config/python_scripts

Create a new file in the python_scripts directory called:

easyplus.py

and copy the python code shown above into it.

Restart Home Assistant.

2 Likes

@123 if this is what i think it is and does what it says: you’re amazing, this perfectly reflects what I imagined!
I have some experience with python_scripts and hass: I use it already for demux rf signals and for youtube.
I will implement and test over the weekend and get back to you.

Very intrigued.

@123 I couldn’t wait any longer: this is INSANE, works straight out of the box and much, much lighter in coding scripts and automations.

Feels like an open source Apple product: well-thought out, fool proof, works out of the box, no confusions and manuals needed. WOW … :champagne::partying_face:

I hope I can pick your brain some more in the future. A whole new world is opening up.

Thanks!

python_scripts provide a lot of power that would be otherwise cumbersome in Automations/Scripts alone. The only downside to @123 's method is…

Assuming you have two things that need the easyplus on to work: a dishwasher and a … clothes washer (I have no idea what an easyplus is, so I’m just making things up here).

If you use this python_script to turn on the dishwasher, everything works as expected. Same for the clothes washer. But… if you use this to turn on the dishwasher, and after some time less than 17 seconds also turn on the clothes washer, the clothes washer will NOT wait the 17 seconds. This is where the binary_sensor.template comes in.

You could keep the binary_sensor.template as I indicated above and alter @123 's script to only continue once that is on. I also don’t like the pile of nested if’s… so I got rid of those:

def doWork(hass, data):
  sn = data.get('script_name')
  ep = hass.states.get('switch.easyplus')
  ep_17 = hass.states.get('binary_sensor.easyplus_on_17s')

  if sn is None:
    logger.warning('<easyplus> No script name was supplied.')
    return

  if ep is None:
    logger.warning('<easyplus> switch.easyplus does not exist.')
    return

  if ep_17 is None:
    logger.warning('<easyplus> binary_sensor.easyplus_on_17s does not exist.')
    return
    
  ss = hass.states.get('script.' + sn)

  if ss is None:
    logger.warning('<easyplus> Supplied script does not exist.')
    return

  if ep.state == 'off':
    service_data = {'entity_id':'switch.easyplus'}
    hass.services.call('switch', 'turn_on', service_data, False)

  while ep_17.state == 'off':
    time.sleep(1)
    ep_17 = hass.states.get('binary_sensor.easyplus_on_17s')

  hass.services.call('script', sn, '', False)
    

doWork(hass, data)

You can get rid of the easyplus_on_17s sensor entirely if you add some code to look at the last_updated time of switch.easyplus. However, personally, I like to have the visual indicator in the UI for troubleshooting that the 17 seconds has elapsed, so I’d keep the binary_sensor. But, if you’d prefer otherwise, this will probably work, though it’s untested code:

def doWork(hass, data):
  sn = data.get('script_name')
  ep = hass.states.get('switch.easyplus')

  if sn is None:
    logger.warning('<easyplus> No script name was supplied.')
    return

  if ep is None:
    logger.warning('<easyplus> switch.easyplus does not exist.')
    return
    
  ss = hass.states.get('script.' + sn)

  if ss is None:
    logger.warning('<easyplus> Supplied script does not exist.')
    return

  if ep.state == 'off':
    service_data = {'entity_id':'switch.easyplus'}
    hass.services.call('switch', 'turn_on', service_data, False)
    ep = hass.states.get('switch.easyplus')

  wait_until = ep.last_changed + datetime.timedelta(seconds=17)
  while wait_until > datetime.now():
      time.sleep(1)

  hass.services.call('script', sn, '', False)
    

doWork(hass, data)

@swiftlyfalling thanks for streamlining even more. I am impressed, surrounded by great minds.

You are right this is a possibility, though an unlikely one, but I really like to make script more robust than it already is. I went for your untested option B or 2 (once again). I prefer it’s logic. Being the bad person I am, I stress tested that scenario out of the box and managed to crash the easyplus.py script. I will restart hass and continue testing, might be an outlier.

I get below error in home assistant:

Log Details (ERROR)
Sat Aug 03 2019 21:12:27 GMT+0200 (Central European Summer Time)
Error executing script: Not allowed to access module.now
Sat Aug 03 2019 21:32:37 GMT+0200 (Central European Summer Time)
Error executing script: name 'logger' is not defined
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/python_script/__init__.py", line 159, in execute
    exec(compiled.code, restricted_globals, local)
  File "easyplus.py", line 31, in <module>
  File "easyplus.py", line 6, in doWork
NameError: name 'logger' is not defined

That’s entirely my fault… the logger, since it’s being used, will need to be passed in to the function as well.

So the last line should read:

doWork(hass,data,logger)

and def doWork(…) should read:

def doWork(hass, data, logger)

That’ll fix it.

It also looks like datetime.now() isn’t correct. It needs to be datetime.datetime.now(). Soo… all together:

def doWork(hass, data, logger):
  sn = data.get('script_name')
  ep = hass.states.get('switch.easyplus')

  if sn is None:
    logger.warning('<easyplus> No script name was supplied.')
    return

  if ep is None:
    logger.warning('<easyplus> switch.easyplus does not exist.')
    return
    
  ss = hass.states.get('script.' + sn)

  if ss is None:
    logger.warning('<easyplus> Supplied script does not exist.')
    return

  if ep.state == 'off':
    service_data = {'entity_id':'switch.easyplus'}
    hass.services.call('switch', 'turn_on', service_data, False)
    ep = hass.states.get('switch.easyplus')

  wait_until = ep.last_changed + datetime.timedelta(seconds=17)
  while wait_until > datetime.datetime.now():
      time.sleep(1)

  hass.services.call('script', sn, '', False)
    

doWork(hass, data, logger)

Also, another thing to note. The nice thing about python_scripts… if they crash like this… you can just edit them and try again. As long as the file is present in python_scripts directory when HASS starts, you can edit it and call it again and the changes will take effect right then.

python_scripts changes: thanks, that saves some time in testing :+1:t4:

@swiftlyfalling implemented your latest script and getting a new error:

Log Details (ERROR)
Sun Aug 04 2019 08:23:59 GMT+0200 (Central European Summer Time)
Error executing script: can't compare offset-naive and offset-aware datetimes
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/python_script/__init__.py", line 159, in execute
    exec(compiled.code, restricted_globals, local)
  File "easyplus.py", line 31, in <module>
  File "easyplus.py", line 25, in doWork
TypeError: can't compare offset-naive and offset-aware datetimes

I was afraid this might happen.

You can try time.localtime() in place of datetime.datetime.now().

seems like a small but hard step yet to overcome:

Log Details (ERROR)
Sun Aug 04 2019 16:02:31 GMT+0200 (Central European Summer Time)
Error executing script: '>' not supported between instances of 'datetime.datetime' and 'time.struct_time'

I’ll need to write a python_script on my end to test the available options for comparing the current time to the last updated time of the sensor. I don’t have time to do that right now, so it’s going to be a bit.

Until then, if you can’t figure it out and someone else doens’t already know the answer, your best bet is to use the easyplus_on_17s binary_sensor template and include that logic in the script (as I posted above).

@swiftlyfalling very grateful already for your efforts, indeed I have already a working solution and implementing py e2e embedded check is great but no sense of urgency, thanks again.

1 Like