Low Battery Template Returns Batteries at 100%

I categorise all my battery operated devices’ battery entities with area: critical_battery like this:

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%

Just for clarification you stated the batteries are defined by area, but your code selects groups:

'group.critical_batteries'

From my experience, Groups will combine all the sensors and report the state as if it’s one entity.

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.

1 Like

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') }}
1 Like

Thanks for the replies.

Yes, but I also said

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.

1 Like

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.

{{ 80 < 90 }}
{{ '80' < '90' }}
{{ 80 < 100 }}
{{ '80' < '100' }}
{{ '8' < '10' }}

Okay, I need to do some reading. I thought I had it, but when I added

{{ 80 == '80' }}
{{ 'eight' == '8' }}

That just confused me no end :laughing:

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.

:smile:

I’m very much a “if it’s not broken, don’t fix it” kinda guy. But it is on my (rather extensive) todo list.

It was broken though :wink:.

1 Like

Good point, well made.

I don’t suppose you know a quick way of adding a label to 115 entities without doing it one at a time?

Yep, that was added in the meantime too. Go to Settings > Devices & services > Entities tab, then follow the below steps:

  1. Filter by Entities (optional, but in your above case you would use Areas > Critical Battery).
  2. Click the multi-select icon and choose all the entities you want to be labelled with the same label.
  3. In the top right, a new option will appear named “Add Label”. Click that, then “Create Label” and fill in the details > Create.
  4. Profit?

1 Like

That is awesome. Thanks :smiley:

1 Like