Jinja / Template Sensor Question

I am creating a low battery sensor which is a list of batteries that are less than 30 percent in my case. My batteries are split among multiple groups. I have the following template sensor which I have checked as much as I can in the template editor. It seems good in the template editor, but has no value when implemented as follows:

  - platform: template
    sensors:
      low_batteries:
        value_template: >-
          {% set lock_batteries = states | selectattr('entity_id', 'in', state_attr('group.door_lock_batteries', 'entity_id')) | selectattr('state', 'lessthan', '30') |map(attribute='name') | list %}
          {% set door_batteries = states | selectattr('entity_id', 'in', state_attr('group.door_sensor_batteries', 'entity_id')) | selectattr('state', 'lessthan', '30') |map(attribute='name') | list %}
          {% set motion_batteries = states | selectattr('entity_id', 'in', state_attr('group.motion_sensor_batteries', 'entity_id')) | selectattr('state', 'lessthan', '30') |map(attribute='name') | list %}
          {% set temperature_batteries = states | selectattr('entity_id', 'in', state_attr('group.temp_sensor_batteries', 'entity_id')) | selectattr('state', 'lessthan', '30') |map(attribute='name') | list %}
          {% set other_batteries= states | selectattr('entity_id', 'in', state_attr('group.other_batteries', 'entity_id')) | selectattr('state', 'lessthan', '30') |map(attribute='name') | list %}
          {% set button_cube_batteries= states | selectattr('entity_id', 'in', state_attr('group.button_cube_batteries', 'entity_id')) | selectattr('state', 'lessthan', '30') |map(attribute='name') | list %}
          {% set low_batteries = lock_batteries + door_batteries + motion_batteries + temperature_batteries + other_batteries + button_cube_batteries %}
          {{ low_batteries | list | join(', ') }}
1 Like

Classic problem of not being able to automatically determine all the entity_id’s from the template. The only entity_id’s it will find are the groups, and so it will only update when one of those groups changes, which I’m sure they don’t. So you need to force this sensor to update more often. One way would be to add:

entity_id: sensor.time

Another would be to use an automation that is triggered periodically and then calls home_assistant.update_entity to update it.

1 Like

My response isn’t a solution (Phil provided the solution, see above) but a suggestion. If you are using version 0.96 or later, it offers the expand function which allows you to streamline your template (see Working with Groups).

  - platform: template
    sensors:
      low_batteries:
        entity_id: sensor.time
        value_template: >-
          {{ (expand('group.door_lock_batteries') + 
              expand('group.door_sensor_batteries') + 
              expand('group.motion_sensor_batteries') + 
              expand('group.temp_sensor_batteries') + 
              expand('group.other_batteries') + 
              expand('group.button_cube_batteries')) | 
              selectattr('state', 'lessthan', '30') | 
              map(attribute='name') | list | join(', ') }}

I’ve tested expand on my system and it works as advertized:

3 Likes

Awesome. That is much cleaner/easier to read than what I had. Much appreciated.

One more question for you guys if you don’t mind. It is about this: selectattr('state', 'lessthan', '30').

It seems to include batteries that have a 100% level. How can I avoid that?

Probably because it’s comparing strings, and in that case, ‘100’ is less than ‘30’.

I was thinking that as well, but I don’t see how to get around it. I tried ‘030’, but it didn’t seem to work. Any ideas?

I found a solution to the 100% battery level issue using the rejectattr function.

          {{ (expand('group.door_lock_batteries') + 
              expand('group.door_sensor_batteries') + 
              expand('group.motion_sensor_batteries') + 
              expand('group.temp_sensor_batteries') + 
              expand('group.other_batteries') + 
              expand('group.button_cube_batteries')) | 
              selectattr('state', 'lessthan', '30') | 
              rejectattr('state', 'equalto', '100') |
              rejectattr('state', 'equalto', '100.0') |
              map(attribute='name') | list | join(', ') }}
1 Like

Just out of curiosity, what happens if you try this (the value 30 is not delimited by single-quotes):

selectattr('state', 'lessthan', 30)

In the template editor I get a message that says Unknown error rendering template

please allow me to join, I am struggling with a related issue I fear.

trying this:

{{ states.geo_location|selectattr('attributes.source','eq','wwlln')|selectattr('state','lessthan',50)|list|count }}

gives me that same error.

tried

{{ states.geo_location|selectattr('attributes.source','eq','wwlln')|select('lessthan',50)|list }}

first, because I thought that to be the instruction in https://jinja.palletsprojects.com/en/2.10.x/templates/#select

but it resulted in the exact same error.

what I am trying to do:

find all states.geo_location, filter those on source, then filter on state < 50, then list, and count

without the filter on state < 50 it works fine.

what I am trying to understand is why I need selectattr, when I in fact am looking for a state. And moreover, specify the attribute to be state…

also we need to quote the number apparently, making it a string. Isn’t this odd? Or is this the same as having to specify |int when dealing with numbers and strings, only the opposite now :wink: Might logically make more sense maybe?

{{ states.geo_location|selectattr('attributes.source','eq','wwlln')|selectattr('state','lessthan','50')|list|count }}

makes it work.

while this:

{{ states.geo_location|selectattr('attributes.source','eq','wwlln')|select('lessthan','50')|list|count }}

still errors out with the above error.

fwiw, this works out also,

          {% set km = states('input_number.lightning_strikes_near') %}
          {{ states.geo_location|selectattr('attributes.source','eq','wwlln')
             |selectattr('state','lessthan',km)|list|count }}

help much appreciated!

So the problem is still that you’re trying to do numeric comparisons on strings, and strings are compared much differently than numbers. You need to compare the states converted to numbers.

Try something like this:

{% set entities = expand('group.door_lock_batteries') +
                  expand('group.door_sensor_batteries') +
                  expand('group.motion_sensor_batteries') +
                  expand('group.temp_sensor_batteries') +
                  expand('group.other_batteries') +
                  expand('group.button_cube_batteries') %}
{% for x in entities if states(x)|int < 30 %}
  {%- if not loop.first %}, {% endif -%}
  {{- states[x.split('.')[0]][x.split('.')[1]].name -}}
{% endfor %}

If you know all of these entities have a friendly name, then you can change:

  {{- states[x.split('.')[0]][x.split('.')[1]].name -}}

to:

  {{- state_attr(x, 'friendly_name') -}}
1 Like

will take this to a separate topic because of a misunderstanding on my behalf

This is really solved by @pnbruckner, not me.
Thank you! I had to make a small edit (which I totally guessed on because I have no idea what I am doing) to make this work. Your example was very helpful. This is what I ended up with:

{% set entities = expand('group.door_lock_batteries') +
                  expand('group.door_sensor_batteries') +
                  expand('group.motion_sensor_batteries') +
                  expand('group.temp_sensor_batteries') +
                  expand('group.other_batteries') +
                  expand('group.button_cube_batteries') %}
{% for x in entities if x.state|int < 30 %}
  {%- if not loop.first %}, {% endif -%}
  {{- x.name -}}
{% endfor %}

and replace the x.name part with this if you want the entity_id:

{% for x in entities if x.state|int < 30 %}
  {%- if not loop.first %}, {% endif -%}
  {{- x.entity_id -}}
{% endfor %}

Thanks again for the help.

1 Like

I was looking to do something similar to this… After using this post for inspiration, I’ve ended up with a generic solution using device_class=battery, so there was no need to hardcode group/entity names.

This template outputs a comma delimited list of entity names where battery level is under 30%:

{% set separator = joiner(", ") %}
{% for battery in (states | selectattr('attributes.device_class', 'eq', 'battery'))  if battery.state | int < 30 %}
{{- separator() + battery.entity_id -}} 
{% endfor %}
2 Likes

I don’t have enough devices to test this properly (and the ones I do have, have fully-charged batteries). Can you do me a favor and try this out on your system and let me know if it produces correct results?

{{ states | selectattr('attributes.device_class', 'eq', 'battery')
    | rejectattr('state', 'eq', '100')
    | selectattr('state', 'lt', '30')
    | map(attribute='entity_id')
    | list | join(', ') }}

It performs a string comparison of the battery-level (which explains why it rejects 100 because in a string comparison, 100 is not less than 30). It assumes all battery-level values are strings. If there’s even one that is int or float, this template will fail.

NOTE
This is just an exercise to see if useful results can be produced without employing a for-loop.

1 Like

It looks like your code works. I like it because it doesn’t require me to update all the groups that have batteries. This is what I get with your code snippet:

1 Like

Just be aware that’s there’s a subtle difference in how often this template will be updated compared to the other version.

In 0.117, a template containing states, and no other identifiable entities, will be evaluated every minute. That’s more than adequate to report a low battery but arguably much more than needed given that batteries last months.

In contrast, a template that expands a group (and contains no reference to states or states.DOMAIN) will be evaluated only when one of the group’s entities changes state. In other words, the template is updated only when something changes (which is more efficient than polling according to a fixed time interval).

You should know that it’s possible (and fairly easy) to create an automation that is triggered at startup and dynamically creates a group containing the entities you desire (like all entities whose device_class is battery). This eliminates the overhead of manually maintaining the group’s membership. The template can then refer to the dynamically-created group.

1 Like

Your code also works. This is what I get:

1 Like

I’ll have to look into that. Below is what I implemented a while back. Lot’s of room for improvement.

#######################################
######## Low Battery Detection ########
#######################################
  - id: low_battery_detection
    alias: Low Battery Detection
    trigger:
      - platform: numeric_state
        entity_id: 
        - sensor.garage_door_lock_battery
        - sensor.front_door_deadbolt_battery
        - sensor.deck_door_deadbolt_battery
        - sensor.basement_door_deadbolt_battery
        - sensor.garage_door_battery
        - sensor.garage_car_door_sensor_battery
        - sensor.laundry_entry_door_battery
        - sensor.front_door_battery
        - sensor.deck_door_battery
        - sensor.basement_stairs_door_battery
        - sensor.basement_door_battery
        - sensor.shop_door_battery
        - sensor.jeremys_closet_door_battery
        - sensor.nicholes_closet_door_battery
        - sensor.finishing_room_door_sensor_battery
        - sensor.master_bedroom_door_sensor_battery
        - sensor.pantry_door_sensor_battery
        - sensor.refrigerator_door_sensor_battery
        - sensor.dust_collector_blast_gate_sensor_battery
        - sensor.finishing_room_blast_gate_sensor_battery
        - sensor.bedroom_hall_motion_detector_battery
        - sensor.computer_room_motion_sensor_battery
        - sensor.computer_room_motion_sensor_desk_battery
        - sensor.living_room_motion_sensor_battery
        - sensor.kitchen_motion_sensor_battery
        - sensor.shop_motion_sensor_battery
        - sensor.shop_entry_motion_sensor_battery
        - sensor.shop_motion_beam_battery
        - sensor.shop_motion_over_saw_battery
        - sensor.dining_room_motion_sensor_battery
        - sensor.dining_room_motion_sensor_battery2
        - sensor.master_bedroom_motion_sensor_battery
        - sensor.master_bedroom_motion_sensor2_battery
        - sensor.basement_lumber_rack_motion_sensor_battery
        - sensor.basement_workout_motion_sensor_battery
        - sensor.basement_play_area_motion_sensor_battery
        - sensor.basement_stairs_motion_sensor_battery
        - sensor.basement_beam_motion_sensor_battery
        - sensor.craft_room_motion_sensor_battery
        - sensor.craft_room_motion_desk_battery
        - sensor.half_bathroom_motion_sensor_battery
        - sensor.master_bath_motion_sensor_battery
        - sensor.master_bath_motion_sensor_battery2
        - sensor.master_bath_shower_motion_sensor_battery
        - sensor.poop_closet_motion_sensor_battery
        - sensor.guest_bathroom_motion_sensor_battery
        - sensor.finishing_room_motion_sensor_battery
        - sensor.finishing_room_door_motion_sensor_battery
        - sensor.laundry_room_motion_sensor_battery
        - sensor.garage_motion_sensor2_battery
        - sensor.main_hall_temp_sensor_battery
        - sensor.guest_bath_temp_sensor_battery
        - sensor.basement_temp_sensor_battery
        - sensor.basement_temp_sensor_dust_collector_battery
        - sensor.shop_cart_temp_sensor_battery
        - sensor.shop_entry_temp_sensor_battery
        - sensor.shop_jointer_temp_sensor_battery
        - sensor.deck_temp_sensor_battery
        - sensor.finishing_room_temp_sensor_battery
        - sensor.finishing_room_temp_sensor2_battery
        - sensor.basement_stairs_temp_sensor_battery
        - sensor.master_bath_temp_sensor_battery
        - sensor.fireplace_temp_sensor_battery
        - sensor.grill_temp_sensor_battery
        - sensor.grill_temp_sensor2_battery
        - sensor.garage_temp_sensor_battery
        - sensor.roomba_battery
        - sensor.fireplace_gas_sensor_battery
        - sensor.water_heater_gas_sensor_battery
        - sensor.shop_smoke_detector_battery
        - sensor.finishing_room_smoke_detector_battery
        - sensor.shop_cube_battery
        - sensor.shop_cube2_battery
        - sensor.jeremys_cube_battery
        - sensor.nicholes_cube_battery
        - sensor.basement_cube_battery
        - sensor.hannahs_cube_battery
        - sensor.shop_button1_battery
        - sensor.shop_button2_battery
        - sensor.shop_button3_battery
        - sensor.shop_button4_battery
        - sensor.shop_button5_battery
        - sensor.shop_button6_battery
        - sensor.shop_button7_battery
        - sensor.shop_button8_battery
        - sensor.shop_button9_battery
        - sensor.living_room_button_battery
        - sensor.master_bath_button_battery
        - sensor.basement_button_battery
        - sensor.laundry_room_leak_sensor_battery
        - sensor.shop_leak_sensor_battery
        - sensor.poop_closet_leak_sensor_battery
        - sensor.backyard_hose_leak_sensor_battery
        - sensor.driveway_hose_leak_sensor_battery
        - sensor.half_bath_leak_sensor_battery
        - sensor.half_bath_sink_leak_sensor_battery
        - sensor.guest_bath_left_sink_leak_sensor_battery
        - sensor.guest_bath_right_sink_leak_sensor_battery
        - sensor.master_bath_jeremy_sink_leak_sensor_battery
        - sensor.master_bath_nichole_sink_leak_sensor_battery
        - sensor.kitchen_sink_leak_sensor_battery
        - sensor.guest_bath_sink_leak_sensor_battery
        - sensor.water_heater_leak_sensor_battery
        - sensor.finishing_room_water_leak_sensor_battery
        - sensor.basement_sink_water_leak_sensor_battery
        below: 25
      - platform: state
        entity_id: input_boolean.ha_initialized
        to: 'on'
    condition:
      - condition: state
        entity_id: input_boolean.ha_initialized
        state: 'on'
    action:
      - service: group.set
        data_template:
          name: Low Battery Levels
          object_id: low_battery_levels
          entities: >
            {% set entities = expand('group.door_lock_batteries') +
                              expand('group.door_sensor_batteries') +
                              expand('group.motion_sensor_batteries') +
                              expand('group.temp_sensor_batteries') +
                              expand('group.other_batteries') +
                              expand('group.leak_sensor_batteries') +
                              expand('group.button_cube_batteries') %}
            {% for x in entities if x.state|int < 25 %}
              {%- if not loop.first %}, {% endif -%}
              {{- x.entity_id -}}
            {% endfor %}
      - condition: template
        value_template: >
          {% if trigger.to_state.name == "Home Assistant Initialized" %}
            false
          {% else %}
            true
          {% endif %}
      - service: notify.text_jeremy
        data_template:
          message: >
            {{ trigger.to_state.name }} level is less than 25 percent
      - service: persistent_notification.create
        data_template:
          title: "Low Battery Warning"
          notification_id: low_battery
          message: >
            {{ trigger.to_state.name }} level is less than 25 percent