Automation Trigger: list of devices

Because List is a string. Remember all templates return strings only. That’s why I purposefully did not add it as an attribute. Anyways, if you want it as a list you need to split it with a comma.

Yea, thanks. Of course I know… duh.

About the comma separation, why does this add a comma to the last:


          {%- for s in states.sensor %}
            {%- if ('battery_level' in s.entity_id or 'motion_sensor_battery' in s.entity_id)
                 %}
              {{ s.entity_id }}{% if not loop.last %}, {% endif %}
            {%- endif %}
          {%- endfor %}

Because of where you placed the if statement. It’s inside the for-loop which iterates through all sensors. If you have 50 sensors and the 45th is the last one that matches the if statement, it will add a comma because there are still five more loops to complete.

Look at the example I posted above. The if statement is on the same line as the for-loop statement (it’s part of the for-loop’s declaration). It constrains the number of iterations (it will loop only the number of times where the if statement evaluates to true). I learned this technique when searching for the Jinja equivalent of a break statement. There is none and the workaround is to constrain the iterations.

          {%- for s in states.sensor if ('battery_level' in s.entity_id or 'motion_sensor_battery' in s.entity_id) %}
              {{ s.entity_id }}{% if not loop.last %}, {% endif %}
          {%- endfor %}

yes! that’s it. I use it in other places, but completely missed that here… Thanks! Now we can use that also as a list of entities to act upon in a service template :wink:

next step would be how to find the ‘diff’, meaning which battery sensor is the new one below the alert_level.

using the entity_id list in the template would be easy of course, but have the downside of the verbose listing.
Can we somehow diff between a from_state and a to_state of these templates?

separate post:
completely forgot we can do this:

automation:
  - alias: Create battery group
    trigger:
      platform: homeassistant
      event: start
    action:
        service: group.set
        data_template:
          object_id: battery_sensors
          entities: >-
            {%- for s in states.sensor
              if ('battery_level' in s.entity_id or
                  'motion_sensor_battery' in s.entity_id)%}
              {{s.entity_id}}{% if not loop.last %}, {% endif %}
            {%- endfor %}

creating the group at startup. combine with

should make life much easier ? (of course if @123 's findings are solved any time soon now)

You have described the exact scenario I wanted to test in 0.110 when the behavior of expand was first enhanced. However, the enhancement proved to be more difficult to implement than anticipated and now might not become available until 0.114.

Here is what I wanted to test:

You have an existing group and you add a member then execute Reload Groups. Will an existing value_template containing expand('group.your_group') automatically compensate and create another listener for the new group member?

I suspect it will not and that makes the enhancement fall short of perfect. You will still have to restart Home Assistant in order to reload the Template Sensor (and update the required listeners).

I recall seeing a PR to provide a reload service for Template Sensors. I lost track of it and don’t know if it was paused or cancelled.

not really sure yet, but I’ve made this:

automation:
  - alias: Create battery group
    trigger:
      platform: homeassistant
      event: start
    action:
      - service: group.set
        data_template:
          object_id: battery_sensors
          entities: >-
            {%- for s in states.sensor
              if ('battery_level' in s.entity_id or
                  'motion_sensor_battery' in s.entity_id)%}
              {{s.entity_id}}{% if not loop.last %}, {% endif %}
            {%- endfor %}
      - service: group.set
        data_template:
          object_id: battery_sensors_low
          entities: >-
            {%- for s in states.sensor
              if ('battery_level' in s.entity_id or
                  'motion_sensor_battery' in s.entity_id)
               and s.state|int < states('input_number.battery_alert_level')|int%}
              {{s.entity_id}}{% if not loop.last %}, {% endif %}
            {%- endfor %}

and the second service creates a group with ‘low’ sensors. This doesnt auto update on changing the slider unfortunately. Easily enough this extra automation:

  - alias: Update battery low group
    trigger:
      platform: state
      entity_id: input_number.battery_alert_level
    action:
      service: automation.trigger
      entity_id: automation.create_battery_group

takes care of that just fine. Of course other triggers could be added there, suppose even one using the discussed expand feature.

Only time this fails now is when 0 sensors are below the threshold:

Create battery group: Error executing script. Invalid data for call_service at pos 2: Entity ID is an invalid entity id for dictionary value @ data['entities']

it won’t create an empty group :wink: should probably use group.remove if count = 0…

btw, the template sensors using the auto created groups are way simpler:

  - platform: template
    sensors:
      low_level_batteries_group:
        friendly_name: Low level batteries group
        value_template: >
            {%- for s in expand('group.battery_sensors')
              if s.state|int < states('input_number.battery_alert_level')|int%}
             {%- if loop.first %}{{loop.length}}{% endif %}
            {%- endfor %}
        attribute_templates:
          List: >
           {% set threshold = states('input_number.battery_alert_level')|int %}
           {%- for s in expand('group.battery_sensors')
            if s.state|int < threshold %}
              {%- if loop.first %}{{loop.length}} batteries are below {{threshold}} %: {% endif %}
              {{s.name + ': ('+ s.state + '%)'}}{% if not loop.last %}, {% endif %}
           {%- endfor %}

and, will become even simpler if the battery_sensors_low group is used.

edit

now using

    action:
      service_template: >
        script.group_battery_sensors_low_{{'remove' if
          states('sensor.low_level_batteries')|int == 0 else 'create'}}

It will become simpler if expand is (ever) fixed but, like I said, it’s unlikely that it will eliminate the need to update Template Sensors via a restart. Although your automation can dynamically build a group, a Template Sensor is assigned listeners only on startup. So if your group adds members after startup, Template Sensors that have expanded that group are oblivious to the additional members. They will only listen for state-changes to the group’s original membership, when the Template Sensor was configured at startup.

The only way I believe this limitation can be eliminated is by providing the ability to reload Template Sensors. That alone would be a valuable addition.

It doesn’t monitor every minute. It monitors every 15 minutes but your point is taken. I could probably cut that back to check once a day and be fine but I don’t see any issues with checking more frequently. And it doesn’t just monitor permanently mounted devices but it also monitors the batteries of mobile devices too. And for those they will normally go dead in a day. So it’s convenient to get info on those devices more often too.

To be fair the package doesn’t just monitor the batteries states. It also creates persistent notifications and handles the notifiers too. Along with supporting ignoring any devices you don’t want to monitor. And it allows for selecting the thresholds in which to get notified from the front-end.

And as I said above, I copied the code from another user a long time back and it worked well (after I tweaked it a bit…) so I never really paid any attention to it since then.

Um, no.

It’s not like there is some kind of physical test that gets done on the device battery itself. It just looks at the state of the battery level already reported to HA. How would that cause the batteries to drain faster?

Fair point. I was thinking of things like remote sensors — if you set them up to be able to report every minute, they wouldn’t last long. The principle there is to wake up infrequently, do a “power-expensive” data transmission (including battery state) and go back to sleep.

It wasn’t a criticism of anyone’s work, just an observation that checking battery levels once a minute may be OTT (although valid for phones etc that are online anyway, and know their state).

1 Like

how to count the 0 ( no loop is created because the ‘if’ isnt met) with this technique?

            {%- for s in expand('group.battery_sensors')
              if s.state|int < states('input_number.battery_alert_level')|int %}
             {%- if loop.first %}{{loop.length}}{% endif %}
            {%- endfor %}

I should have been more specific. The first automation runs ever minute and calls script.battery_check. That led me to believe it checks every minute. However, on closer inspection, I can’t find script.battery_check so now I’m not sure what it’s actually doing. Nevertheless, you are correct and, based on what I can see, it checks levels every 15 minutes.

There are several other automations using Time Pattern Trigger including one that re-builds the battery group every minute.

automation.update_battery_status_group_members

Instead of running every minute, maybe that one could monitor the “entity_registry_updated” event (i.e. only when an entity is added/modified).

Anyhow, if it works, great. It’s just a lot more code, running a lot more frequently, than I thought necessary to report a low battery level. It appears that notification management represents a sizeable chunk of it.

None of the sensors I use are battery powered so this subject is mostly academic for me (i.e. the challenge of composing the solution).

The example you posted can’t report 0 because, if the if statement has no matches, it will never enter the for-loop (i.e. zero iterations).

Given that loop.* variables are only defined within a loop, you can’t test them outside the loop and, using this “constrained for-loop technique” you can’t use them to test inside the loop when there are zero iterations.

One possible workaround is to define a namespace variable, before entering the for-loop, and initialize it to 0. Increment it within the loop (set it to loop.index) then check its value after exiting the loop.

ya, so we’re back to this:

          {%- set ns = namespace(below=[]) %}
          {%- for s in expand('group.battery_sensors')
            if s.state|int < states('input_number.battery_alert_level')|int
                %}
              {%- set ns.below = ns.below + [s] %}
          {%- endfor %}
            {{ns.below|count}}

?
seems a bit heavy simply for counting…

I tried this:

{% set alert_level = states('input_number.battery_alert_level') %}
 {{ expand('group.battery_sensors')
         |selectattr('state', 'lt', alert_level)|list|count}}

but somehow it keeps counting all sensors with state 100% …checking the template with only |list makes clear which sensors it selects. Cant get my head around that at the moment…

and

only thing I can think of is it is comparing a string to a number (its always that…) but don’t see how to adapt this template. if I |int the alert_level, it errors, as does it do so when talking out the quotes on a number in the selector. So, the states are strings.
How then to make an int of the state in

|selectattr('state', '<', '50')

so I can compare these to the inted alert_level|int ?

Crap! You’re right!

That was a “left-over” from some other testing that I had been doing at the time and somehow the script never got moved into the package. and I never removed the automation.

Thanks for finding that easter egg for me. Now I can take both the automation and the script out. :laughing:

But I’m sure my config is littered with a whole bunch of that kind of code from testing things that turned out to be a dead end.

will take this to a separate thread, otherwise well be to far off off the main topic I fear.

this won’t work unless the state has the same number of characters.

The following is True when evaluating strings:

'9' > '50'

This is because the first character '9' is greater than the first character '5'. Hence the reason I always stay away from the time comparisons using string that everyone and their mother uses on this forum. But it’s a personal preference with that because time should always have the same number of characters and resolve correctly.

thanks Petro, and yes, that’s why I took it to the dedicated thread here, and @123 solved it for me, in an up to now for me unknown (and undocumented somehow) way. Using a double |map() filter. Never would have guessed the second map('int') would int all states of the previous map().

for reference here:

          {% set alert_level = states('input_number.battery_alert_level')|int %}
          {{expand('group.battery_sensors')
            |map(attribute='state')|map('int')
            |select('<',alert_level)
            |list|count}}

using the now automatically generated group.battery_sensors_low it can even be as short as:

{{expand('group.battery_sensors_low')|count}}

of course there’s always a new challenge: how to template for ‘all batteries are fine’, if the group.low_level_batteries_group is automatically removed, and no listing can be done, so I need to check for the existence of the group:

          {% if 'group.battery_sensors_low' is defined %}
          {% set entities = expand('group.battery_sensors_low') %}
          {{entities|count}} batteries are low:
          {{entities
            |map(attribute='name') 
            |join(', ')}}
          {% else %} All batteries fine
          {% endif %}

doesn’t work, and simply goes into the if, instead of the else…
or would it still have to be:

          {% if states('group.battery_sensors_low') != 'unknown' %}
          {% set entities = expand('group.battery_sensors_low') %}
          {{ entities|count}} batteries are low:
          {{entities
            |map(attribute='name') 
            |join(', ')}}
          {% else %} All fine
          {% endif %}

would have thought the ‘defined’ technique could make it shorter/directer?

it would, but you aren’t checking the state object. You’re checking a string that you made in the if statement. Therefore it will always resolve to True.

first things first:
I can’t use the group ==/!= ‘unknown’ because the state of the group always is ‘unknown’ even if it exists…

So I have to use another check for existence. ‘is defined’ won’t do it as described above. What other options do we have in this case? Only a ‘hack’ like this (counting an non existing group…)

expand('group.battery_sensors_low')|count > 0

the does seem to work correctly now:

          {% set entities = expand('group.battery_sensors_low') %}
          {% set threshold = states('input_number.battery_alert_level')|int %}
          {% if entities|count > 0 %}
          {{entities|count}} batteries are below {{threshold}} %:
          {{entities
            |map(attribute='name')
            |join(', \n')}}
          {% else %}
            All batteries above {{threshold}} %
          {% endif %}