I have a script that runs whenever I add a device or restart Home Assistant that updates a group.critical_batteries
I then use that group in this template to find any batteries below 30%
{% set batts = expand('group.critical_batteries')
| rejectattr('state', 'eq', '100')
| selectattr('state', 'lt', '30') | list %}
{% for bat in batts %}
{{ bat.name }} is at {{ bat.state }}%
{% endfor %}
All working fine until a couple of days ago, coincidentally when I updated to HA2025.5.3.
Now, this template returns 3 devices with batteries at 100%
Lean to Greenhouse Door Battery is at 100.0%
Garage Door Left Battery is at 100.0%
Poly Tunnel Temperature Battery is at 100.0%
Interestingly, I noticed that these devices are all in my ZHA network and similar devices in my Zigbee2MQTT network that are at 100% do not report.
What changed, if anything, in the way that ZHA reports battery values for these devices that they now report as low batteries with 100%? I have several other batteries reporting in ZHA that are below 100% and they don’t report as low, it’s only those with 100%
Your template is evaluating the battery states as strings. It will only reject states that are exactly equal to the string "100" — so if the ZHA integration has changed the full state from "100" to "100.0" they won’t get rejected, and they get listed as the string "100.0" is less than (alphabetically earlier than) "30", in the same way that "eight" is “less than” "seven" in the dictionary.
That template will also not list 8% as low, for example, because the string "8" is greater than the string "30".
You will need to do something like this to convert the state strings to numbers first (rule 3 here, which is more “essential” than “advised” if you want correct results):
{% set batts = expand('group.critical_batteries') %}
{% set states = batts|map(attribute='state')|map('float', default=0) %}
{% set names = batts|map(attribute='name') %}
{% for name, state in zip(names, states) -%}
{% if state < 30 -%}
{{ name }} is at {{ state }}%
{% endif -%}
{% endfor %}
That will also automatically reject full batteries — you had to do that as a workaround before because "100" is less than "30" in string comparison.
Troon has explained how to fix it. If you intend to perform an arithmetic comparison of values, an entity’s state value is always a string so it must first be converted to a number.
In addition, the zip function is employed to minimize the amount of code needed to produce the desired output.
If you find the demonstrated technique to be intriguing, notably the use of zip, here’s another example of it (for a very similar application).
Example
{% set s = expand('group.critical_batteries') | selectattr('state', 'is_number') | list -%}
{% set n = s | map(attribute='name') | list -%}
{% set v = s | map(attribute='state') | map('int', 0) | list -%}
{{ zip(n, v) | selectattr(1, '<', 30) | map('join', ' is at ') | join('\n') }}
That script takes all the entities in the critical battery area and drops them into the critical batteries group.
Thanks, yes I eventually figured that out. However, my question was twofold, because I like to figure out why a thing is not working as I expect it to, especially when it was working just fine before. That means something changed and it wasn’t on my end so it must be something in Home Assistant.
Anyway, it’s fixed now. Thanks for the suggested correction. That zip thing is quite interesting.
Entity state values have always been, and continue to be, strings.
Regardless if the entity’s state value appears to be a number, boolean, list, etc it is a string. That aspect of Home Assistant remains unchanged.
Any attempt to perform a numerical comparison of entity values must take this long-established fact into consideration. The entity’s state value must first be converted from string to number before comparing it to another number.
If the string values are not converted to numbers, you get a string comparison which, when comparing numeric strings, does not behave like a numerical comparison.
Copy-paste the following template into the Template Editor and experiment with it to see how string comparisons behave with numeric strings.
The first one reports False because it’s comparing a number (80) to a string ('80'). It would report True if the template converted the string to a number.
{{ 80 == '80' | int(0) }}
The second one reports False because it’s comparing dissimilar strings. It’s possible to convert '80' to a number with int but there’s no built-in Jinja2 filter to convert English words to numbers.
PS. I know you fixed your issue and that you’ve been here since before the feature was implemented, but you really don’t need to use fake areas any longer.
We now have Labels available which bypass the need to create dummy areas.