Variables in script condition template

Hello. I need your guys help setting up a script which takes an entity variable and uses that variable inside a template condition too.

What I need:

  • turn on a light (when motion is detected);
  • wait a few minutes based on a datetime input;
  • turn off the light, only if it’s state was not changed in the meantime (while waiting above).

My script, which I run from a few automations:

light_turn_on_movement:
    sequence:
      - service: light.turn_on
        data_template:
          entity_id: "light.{{ light_entity }}"
      - delay: "{{ states('input_datetime.light_movement_timeout') }}"
      - condition: template
        value_template: >
          {% set last_update = as_timestamp(states.light.{{ light_entity }}.last_updated) %}
          {%  set timeout = state_attr('input_datetime.light_movement_timeout', 'timestamp') | int %}
          {{ (now().timestamp() - last_update - timeout) | int | abs <= 1 }}
      - service: light.turn_off
        data_template:
          entity_id: "light.{{ light_entity }}"

The issue is, of course, in the value_template getting the last_update value.
This statement is not correct:

as_timestamp(states.light.{{ light_entity }}.last_updated)

And I can’t find the correct approach to do it!
I tried:

  • as_timestamp(states.light.[light_entity].last_updated)
  • as_timestamp(states.light.~light_entity~.last_updated)

“Check configuration” passes ok, but reloading the scripts throws this nice error:

config for [script]: invalid template (TemplateSyntaxError: expected name or number) for dictionary value @ data[‘sequence’][2][‘value_template’]. Got None. (See ?, line ?).

Hi, I think

should be like that:

{% set last_update = as_timestamp(states.light[light_entity].last_updated) %}

Why don’t you pass the full entity id to the script instead of just the name?

If you pass the full entity id (incl. light.) you can then use state_attr(light_entity, 'last_updated'), this notation also avoids errors on startup, see here.

1 Like

Thank you for the answers.
In the meantime I found a final and better solution using “wait_template”.

I have this script:

light_turn_on_movement:
  mode: parallel
  sequence:
    - condition: template
      value_template: >-
        {% if is_state(light_entity, 'off') %}
          true
        {% else %}
          {{ (as_timestamp(states.light_entity.last_updated)|int - automation_last_triggered) | abs <= 2 }}
        {% endif %}

    - variables:
        brightness: 0

    - service: light.turn_on
      data:
        entity_id: "{{ light_entity }}"
        brightness_pct: "{{ light_brightness | default(100) }}" #default to 100 if not specified, so that light turns on

    - delay: "00:00:02" #give the light chance to update it's state
    - variables:
        brightness: "{{ state_attr(light_entity, 'brightness') | int }}"

    - wait_template: "{{ is_state(light_entity, 'off') or state_attr(light_entity, 'brightness')|int != brightness }}"
      timeout: "{{ states('input_datetime.light_movement_timeout') }}"

    - condition: template
      value_template: "{{ not wait.completed }}"

    - service: light.turn_off
      data:
        entity_id: "{{ light_entity }}"

Which then I call from my automations, like so:

- alias: Light / Hallway / Movement / Upstairs Bathroom Corner / Turn on
  trigger:
    - platform: state
      entity_id: binary_sensor.paradox_zone_casa_scarii_open
      from: "off"
      to: "on"
  mode: restart
  variables:
    automation_last_triggered: "{{ as_timestamp(states.automation.light_hallway_movement_upstairs_bathroom_corner_turn_on.attributes.last_triggered | default(0)) | int }}"
  action:
    service: script.light_turn_on_movement
    data:
      automation_last_triggered: "{{ automation_last_triggered }}"
      light_entity: light.sonoff_upstairs_bathroom_corner

This approach works very well until now.
It restarts the timeout every time the motion is detected and stops the automation if the light is controlled manually (state or brightness are changed - I don’t monitor other attributes as I don’t have the need for them).

1 Like

I’m at a bit of a loss. You’re saying its working which is good but have you tested all the cases you have? Because your first conditional has an issue, specifically this bit:

as_timestamp(states.light_entity.last_updated)|int

This is always 0. What this is doing is its looking for an entity literally called light_entity and getting its last_updated time. If one doesn’t exist (and it won’t since all entities in HA are prefixed with a domain) then the int filter will simply return 0. So if your logic gets to that else it is going to be true 100% of the time.

I assume what you actually want here is to compare the last_updated time of the passed in light entity to the automation trigger time. In that case you need to adjust that bit to this:

(as_timestamp(states[light_entity].last_updated)|int

This will then look for the state of the entity passed in to the automation and get its last_updated time then compare it to the automation trigger time.

The other potential issue I see is with your automation_last_triggered variable. I notice you have this in there:

variables:
    automation_last_triggered: "{{ as_timestamp(states.automation.light_hallway_movement_upstairs_bathroom_corner_turn_on.attributes.last_triggered | default(0)) | int }}"

I just did some quick testing with this on my own HA. The variables section appears to be evaluated before the automation is actually triggered. Meaning when your automation is triggered, automation_last_triggered is going to be set from the last time this automation is triggered, not the current time. Is that what you want or did you want it to be set to essentially now()?

1 Like

You’re correct, I thank you for your feedback! I have spoken too soon and only did a few number of tests which did not reveal all issues.
I was just investigating the reason it’s not working properly :smile:
One reason was, as you perfectly noted, that I was not getting the proper entity data. By using states[light_entity] it’s working.

The other note you make is actually intentional. I also saw that the variables section is evaluated before trigger, and it’s exactly what I need, for this reason: if the light entity was updated by the automation, only them re-enter the automation again (mode: restart) and keep the light on.
If the light entity was modified outside the automation, leave it as it is.

Now, I face another nice challenge for which, currently, I see no solution:

  • the first time the automation is triggered, the light is off therefore it will be turned on and it’s last_updated changed;
  • the second automation trigger (while the initial is still running) still work ok because the light.last_updated == initial_automation.last_triggered; but this time the light is already on so turning it on again will not alter it’s last_updated value;
  • from now on, all subsequent triggers will stop at the first condition until I manually turn off the light.

If you have any ideas, please share :smiley:

So if I’m understanding correctly the reason that conditional exists is really just to make sure you don’t automatically change the brightness of the light if someone has manually adjusted it, right? I think the way you want to do that is just by changing this first bit here:

    - condition: template
      value_template: >-
        {% if is_state(light_entity, 'off') %}
          true
        {% else %}
          {{ (as_timestamp(states.light_entity.last_updated)|int - automation_last_triggered) | abs <= 2 }}
        {% endif %}

    - variables:
        brightness: 0

    - service: light.turn_on
      data:
        entity_id: "{{ light_entity }}"
        brightness_pct: "{{ light_brightness | default(100) }}" #default to 100 if not specified, so that light turns on

    - delay: "00:00:02" #give the light chance to update it's state

to this:

- choose:
  - conditions: "{{ is_state(light_entity, 'off') }}"
    sequence:
    - service: light.turn_on
      data: 
        entity_id: "{{ light_entity }}"
        brightness_pct: "{{ light_brightness | default(100) }}" #default to 100 if not specified, so that light turns on
    - delay: "00:00:02" #give the light chance to update it's state

So what will happen is this will turn on the light to the specified brightness only if it is off (then a small delay so the state is updated). If the light is on (or after this choose block finishes if it was off) then it will always proceed to the next part (storing the light’s brightness and waiting until it is turned off, its brightness changes or the timeout expires).

Also are you sure you want the mode of this script to be parallel? Seems like you would want it to be restart. Otherwise if someone is in the bathroom longer then you have the timeout set I think its going to turn off the lights on them, even if they are moving around. Since the script running from the first movement is eventually just going to timeout and then turn off the lights, regardless of the other instances running in parallel for additional movement.

EDIT: I didn’t realize you couldn’t use templates for entity_id in a state condition. Correcting the template above to a template condition instead.

Thank you, this fixes my issue but adds another one.

Not only that, because not all my lights support brightness.
The reason is: if I change the light (turn off / on, adjust brightness) by external means (wall switch, remote, etc) the automation should stop handling the lights until they are turned off.
Use-case:

  • I enter living => automation turns on lamp (I have no brightness here)
  • waits 2 minutes and then turns off the lamp because no movement detected
  • but I am on the couch reading something, and I did not want the light to actually turn off
  • I should turn off then turn on the lamp again, this should make the automation stop handling the light and let it on until I turn it off manually

Let’s have this use-case:

  • the automation is running, I adjust the light externally (change the brightness for example) => the automation will stop at "{{ not wait.completed }}"
  • the automation is triggered again by movement, but the light is still on => the condition is not met, it jumps to wait_template which will timeout and turn off the light.

In this use-case the light should not be turned off by the automation because I adjusted it externally.
It should be picked up by the automation again only when I turn it off and motion is detected.
I’m not sure if my intentions are clear enough from my description.

Yes, that’s intended. The script is parallel but the automations are restart.
The script is called by more than one automation for handling different lights. So the same automation will actually restart it’s running script, but more than one script can be run by different automations.
I hope I properly understood this mechanism. And from my tests it’s behaving as expected.

This makes sense. I still think my proposal with the choose supports this use case? Since you aren’t moving there’s only one automation trigger. It will turn on the lamp then begin to wait two minutes. If you flick the light off the wait_template kicks out and ends the script (since the light is off). Then when you turn it back on nothing is watching it so it just stays on.

Although if you do move again then the script will start running again. The second time through it will skip the choose (since the light is already on) and just begin waiting. If it waits for 2 minutes then it will turn off the light again. That seems like a good thing since it means it turns the light off for you when you walk away, right?

Although I’m curious, do your lights which don’t support dimming allow you to specify a brightness_pct in the the light.turn_on service call and have a brightness attribute? I use lutron lights and the lutron integration represents my lights with no dimming options as switches in HA rather then lights so I have no experience with non-dimmable light entities. Just wanted to check on that.

Right. All this should still happen in my proposal. In case I wasn’t clear, I was suggesting leave all this, just replace the first few steps of your script with my choose block. Here is what I was suggesting for the final script in full:

sequence:
- choose:
  - conditions: "{{ is_state(light_entity, 'off') }}"
    sequence:
    - service: light.turn_on
      data: 
        entity_id: "{{ light_entity }}"
        brightness_pct: "{{ light_brightness | default(100) }}" #default to 100 if not specified, so that light turns on
    - delay: "00:00:02" #give the light chance to update it's state

- variables:
    brightness: "{{ state_attr(light_entity, 'brightness') | int }}"

- wait_template: "{{ is_state(light_entity, 'off') or state_attr(light_entity, 'brightness')|int != brightness }}"
  timeout: "{{ states('input_datetime.light_movement_timeout') }}"

 - condition: template
   value_template: "{{ not wait.completed }}"

- service: light.turn_off
  data:
    entity_id: "{{ light_entity }}"

So all the situations you described with the wait template + condition (not turning it off if you flick the light on and off or change the brightness) should still all work. I’m just suggesting a simpler initial part, comparing the last_updated time to the last_triggered time seems like unnecessary complexity (and as you pointed out, isn’t working).

That isn’t how it works. Scripts are separate things from automations and the run mode of the automation won’t affect the run mode of that script. The action section of an automation is essentially a script in itself, so when you set the run mode of an automation you are setting the run mode for that set of actions itself. But if you have an action that launches a script, that runs separately with its own run mode.

At least that’s my understanding. But I don’t want you to fix something which isn’t broken so keep it if its working for you. The specific use case I have a concern with based on that config would be this:

  1. Walk into the room so the light turns on
  2. Continue moving around the room for the duration of the timeout (might want to set it low for this test case) but don’t adjust the light otherwise
  3. Notice the light turns off after the timeout has passed since you entered the room despite you moving around since the first instance of the script has now finished.

If this either doesn’t happen to you or doesn’t bother you based on how you intend to use your lights then feel free to ignore.

This is not a good thing in my scenario. You could be moving around, not really leaving the room.
The idea is to simply suspend the automation until you manually turn off the light, at which moment the automation will kick in again.

Yes, they support calling with that attribute and they simply ignore it.
So for lights that don’t have brightness I don’t specify that parameter to the script, so it will default to 100. They turn on as expected.
A brightness value of 0 turns off the light, any value greater than 0 will turn on the light.

Indeed that’s how I changed and used it.

They seem to work as I’ve expected. Testing with mutiple lights (multiple automations) calling the same script, and triggering multiple time, behaves as expected.
So the automation mode is separate from the script mode. Each automation use it’s set mode and will instantiate it’s own script object. But all script objects share the same mode set in the script.

I have finally a working solution which employs turning off the automation altogether while it’s not supposed to handle the light.
This is the script:


  light_turn_on_movement:
    mode: parallel
    sequence:
      - choose:
        - conditions: "{{ is_state(light_entity, 'off') }}"
          sequence:
            - service: light.turn_on
              data: 
                entity_id: "{{ light_entity }}"
                brightness_pct: "{{ light_brightness | default(100) }}" #default to 100 if not specified, so that light turns on

            - delay: "00:00:02" #give the light chance to update it's state

      - variables:
          brightness: "{{ state_attr(light_entity, 'brightness') | int }}"

      #this wait template defines the "override automation" command
      # if the light supports brightness then changing it will issue the command
      # if brightness is not supported, the command is given by turning off then turning on the light
      # 
      # when such a command is received the automation should stop processing the light until the light is turned off manually
      - wait_template: "{{ is_state(light_entity, 'off') or state_attr(light_entity, 'brightness')|int != brightness }}"
        timeout: "{{ states('input_datetime.light_movement_timeout') }}"

      - choose:
        - conditions: "{{ wait.completed }}" #override command received
          sequence:
            - service: automation.turn_off
              data:
                entity_id: "{{ automation_entity }}"
                stop_actions: false #leave the currently running action active so it will reactivate the automation

            #wait for the override command to be completed (light should be turned on again shortly)
            # if brightness is supported, the wait template will complete instantly
            - wait_template: "{{ is_state(light_entity, 'on') }}"
              timeout: "00:00:10" #allow 10 sec to switch light back on 
            
            - choose:
              - conditions: "{{ wait.completed }}" #override command is complete
                sequence:
                  #wait for the light to turn off, only then restart automation
                  # this also provides fallback in case the light was not turned on within 10 seconds above,
                  # the automation will restart
                  - wait_template: "{{ is_state(light_entity, 'off') }}"
  
            #turn the automation back on, once the light is turned off
            - service: automation.turn_on
              data:
                entity_id: "{{ automation_entity }}"

        default:
          service: light.turn_off
          data:
            entity_id: "{{ light_entity }}"

And this, an automation that uses it:

  - alias: Light / Hallway / Movement / Upstairs Bathroom Corner / Turn on
    initial_state: "on" #force initial state on to ensure automation does not remain off in unexpected scenarios (HA restart for example) if the script did not complete successfully
    trigger:
      - platform: state
        entity_id: binary_sensor.paradox_zone_casa_scarii_open
        from: "off"
        to: "on"
    mode: restart
    action:
      service: script.light_turn_on_movement
      data:
        automation_entity: automation.light_hallway_movement_upstairs_bathroom_corner_turn_on
        light_entity: light.sonoff_upstairs_bathroom_corner

I would have liked to be able to automatically find the automation entity_id within the script, but I was unable to get consistent results.
I used this approach at script start, which sometime return the correct entity, but most of the time do not:

      - variables:
          #get the automation entity ID that called this script
          automation_entity: >
            {% set id = states.script.light_turn_on_movement.context.parent_id %}        
            {% set obj = states.automation | selectattr('context.parent_id','eq', id) | list | first %}
            automation.{{ obj.object_id }}
1 Like

Looks good, glad its working for you.

Is there one automation per light? If so you could customize your light entities using customize.yaml and add a custom attribute to each of automation_id with the value set to its controlling automation. Then in your script you could pull the name of the automation from the attributes of the light_entity.

Only if the parameter bugs you, its not a huge deal either way. I personally like how that would keep the script focused on the light entity though and allow you to keep the mapping between lights and their controlling automations in an external file (customize.yaml in this case). But I’m also all about not fixing things which aren’t broken :slight_smile:

Thank you for your help!

Yes there is, different lights are controlled by different sensors.
I just today learned about being able to add custom attributes via customize.yaml. That’s a really nice feature.
I might be including your suggestion too in the script, once I’m sure the automation entity ID will remain constant :slight_smile: It’s easier to provide it from the same place where the automation is defined, in case it gets changed more (I’m not yet set on a naming format for automations).

1 Like