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
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 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).
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.
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 %}