Struggling to create dynamic battery group and sensors

I am attempting to create a mechanism to detect and inform (via dashboard chit display) the number of batteries that require attention based on a given % threshold. My aim is to never have to manually maintain this list, but to use a dynamically generated group whose members are automatically set based on device class, then produce a count of the number of entities within that group have fallen below a certain battery %.

Sounds simple but I’ve been banging my head on this for the better part of a day.

I have had some success; I have created a script that will automatically generate a group of the battery entities I care about (I have other reasons for wanting these to be in a group):

alias: Create Batteries Group
sequence:
  - service: group.set
    data_template:
      object_id: battery_sensors
      name: Batteries
      entities: >-
        {%- for s in states.sensor | selectattr('attributes.device_class', '==',
        'battery') if is_number(s.state)
          %}
          {{s.entity_id}}{% if not loop.last %}, {% endif %}
        {%- endfor %}
mode: single
icon: mdi:home-battery

The idea is I will run this regularly, and this correctly produces the group I want:

Next, I set about creating a template sensor that will calculate the count of batteries whose power level (state) is below 20. This produces what I want in the template editor (note the result value of 2):

However, when I try to setup a template sensor with this, it’s always 0:

template:
  - trigger:
      - platform: time_pattern
        minutes: "/1"
    number:
      - name: "Low Battery Count"
        state: >
          {% set count = namespace(value=0) %}
          {% for b in expand('group.battery_sensors') if is_number(b.state) %}
            {% if b.state|int < 20 %}
              {% set count.value = count.value + 1 %}
            {% endif %}
          {% endfor %}
          {{ count.value }}
        set_value:
          delay: 5
        step: "1"

(I have this set to every minute just for testing purposes)

It’s always 0:

I have plans for several similar patterns (e.g. how many doors are open, etc) so I really want to figure this out. What am I missing? Or is there a simpler way to do this?

You are not the first to want this and possibly you can look here too, saves more work …if it it what you need
maxwroc/battery-state-card: Battery state card for Home Assistant (github.com)

1 Like

The card mentioned above is fine if it’s only about visualising battery levels. If you want to send notifications and such it gets more complicated.

I’ll share my solution, based on tips and things I gathered from the forum. Copy and pasting on mobile, so please excuse typos and the code/config posted without trimming it.

You can actually add custom attributes to sensors, e.g. note the monitor attribute here. You can also use these attribute values to filter on in the battery state card.

sensor.main_bedroom_ht_battery:
  friendly_name: Main Bedroom H&T Battery
  monitor: True

In my case, I wanted to do two things: At a set time daily, notify me about batteries below a certain threshold, but also notify me when any battery completely runs flat at that moment in time.

For the latter, I needed this. Note the use of the custom attribute.

- sensor:
    # https://community.home-assistant.io/t/trigger-an-automation-based-on-a-groups-individual-entity-state-change/383560/2
    # https://community.home-assistant.io/t/single-automation-for-all-lights/375028/7
    - name: Number of Flat Batteries
      icon: mdi:battery-alert-variant-outline
      state: >
        {% set level = 0 %}
        {% set flat_batteries =
             states.sensor
               | selectattr('attributes.monitor', 'defined')
               | selectattr('attributes.monitor', 'eq', True)
               | rejectattr('state', 'in', ['unavailable', 'unknown', 'none'])
               | selectattr('attributes.device_class', 'eq', 'battery')
               | map(attribute='state')
               | map('int')
               | select('eq', level)
               | list
        %}
        {{ flat_batteries | count }}

Here are my automations.

- alias: "Check For Low Batteries"
  initial_state: true
  variables:
    level: 5
    # https://community.home-assistant.io/t/variable-in-automation-not-working-as-expected/431591/2
    monitored_batteries: >-
      {{
        states.sensor
          | selectattr('attributes.monitor', 'defined')
          | selectattr('attributes.monitor', 'eq', True)
          | rejectattr('state', 'in', ['unavailable', 'unknown', 'none'])
          | selectattr('attributes.device_class', 'eq', 'battery')
          | map(attribute='entity_id')
          | list
      }}
  trigger:
    # what if the server is down at this time? for now, we don't care: batteries might last till the next day and there's a flat battery check
    platform: time
    at: "09:00:00"
  condition: >-
    {{
      expand(monitored_batteries)
        | map(attribute='state')
        | map('int')
        | select('lt', level)
        | list
        | count
        > 0
    }}
  action:
    - service: notify.mobile_app_ceres
      data:
        # icon: https://companion.home-assistant.io/docs/notifications/notifications-basic/#notification-icon
        #       https://companion.home-assistant.io/docs/notifications/actionable-notifications/#icon-values
        title: "Batteries"
        # https://community.home-assistant.io/t/recommended-ways-to-manage-devices-and-entities-names/243815/12
        message: >
          The following devices have less than {{ level }}% charge:
          {%- for b in monitored_batteries %}
            {%- if states(b) | int < level and not is_state(b, 'unavailable') %}
            - {{ state_attr(b, 'friendly_name') | replace(' Battery', '') }}: {{ states(b) | int }}%
            {%- endif -%}
          {%- endfor %}
        data:
          group: "batteries"
          url: homeassistant://navigate/lovelace/devices
          # https://companion.home-assistant.io/docs/notifications/notifications-basic/#notification-icon
          # https://community.home-assistant.io/t/mobile-notification-icon-not-showing-up/408050
          # icon_url: "https://github.com/home-assistant/assets/blob/9b782fe562cbd4e6139f9be17d8e7befafa5f945/logo/logo-small.png?raw=true"

# https://community.home-assistant.io/t/why-isnt-there-a-groups-entities-state-trigger/467179
# https://community.home-assistant.io/t/unleash-the-power-of-expand-for-template-sensors/136941/22
# https://community.home-assistant.io/t/trigger-an-automation-based-on-a-groups-individual-entity-state-change/383560
# https://community.home-assistant.io/t/single-automation-for-all-lights/375028/8
# todo: alert when battery at 1%
- alias: "Check For Flat Batteries"
  initial_state: true
  variables:
    # fix test value
    level: 0
    monitored_batteries: >-
      {{
        states.sensor
          | selectattr('attributes.monitor', 'defined')
          | selectattr('attributes.monitor', 'eq', True)
          | rejectattr('state', 'in', ['unavailable', 'unknown', 'none'])
          | selectattr('attributes.device_class', 'eq', 'battery')
          | map(attribute='entity_id')
          | list
      }}
  trigger:
    - platform: state
      entity_id: sensor.number_of_flat_batteries
  condition: >-
    {{
      expand(monitored_batteries)
        | map(attribute='state')
        | map('int')
        | select('eq', level)
        | list
        | count
        > 0
    }}
  action:
    - service: notify.mobile_app_ceres
      data:
        title: "Batteries"
        message: >
          The following devices have no charge:
          {%- for b in monitored_batteries %}
            {%- if states(b) | int == level and not is_state(b, 'unavailable') %}
            - {{ state_attr(b, 'friendly_name') | replace(' Battery', '') }}
            {%- endif -%}
          {%- endfor %}
        data:
          group: "batteries"
          url: homeassistant://navigate/lovelace/devices

EDIT:

  • I also want aggregated notifications for the daily notification; not one per battery.
  • I don’t want notifications for all batteries.
  • I don’t want to maintain a group.

More info …for the message, I am using a blueprint

1 Like

Thank you Pieter, you’ve given me a new direction. I want to do something like this, except the opposite – I want to automatically look at all batteries, unless I add a custom attribute to exclude them. So far I have this (in customize.yaml):

sensor.amanda_s_iphone_battery_level:
  excludeFromMonitor: true

And then the template looks like this:

{% set level = 20 %}
{{
  states.sensor
    | selectattr('attributes.excludeFromMonitor', 'undefined')
    | rejectattr('state', 'in', ['unavailable', 'unknown', 'none'])
    | rejectattr('attributes.device_class', 'undefined')
    | selectattr('attributes.device_class', 'eq', 'battery')
    | map(attribute='state')
    | map('int')
    | select('lt', level)
    | list
}}

The first filter isn’t perfect, because it will reject an entity if it has excludeFromMonitor: false; I’m not sure how to get around that yet with template filtering. But it’s good enough for now.

The bigger problem is that apparently not all my batteries have numeric states, so trying to figure out how to handle that in the template filters…this is what the above generates now:

ValueError: Template error: int got invalid input 'Full' when rendering template '{% set level = 20 %}
{{
  states.sensor
    | selectattr('attributes.excludeFromMonitor', 'undefined')
    | rejectattr('state', 'in', ['unavailable', 'unknown', 'none'])
    | rejectattr('attributes.device_class', 'undefined')
    | selectattr('attributes.device_class', 'eq', 'battery')
    | map(attribute='state')
    | map('int')
    | select('lt', level)
    | list
}}' but no default was specified

Cool!

In my case, I don’t actually use the value in the attribute, so the assumption is that it will be true or undefined.

In your case, I think you will need to create three lists and merge them: one as you have, another for the false case and then one for the non-numeric batteries. For the latter, you should be able to apply a Jinja test to filter those. I can check tomorrow (I just hit the bed). How would you like to map those states?

I figured out that the non-numeric states were duplicative of sensors that were also available with numbers, like for my iPad:

So in the end, I can achieve what I want by filtering out anything that doesn’t have % as its unit_of_measurement, with the assumption that if the excludeFromMonitor attribute is present, it’s true:

{% set level = 20 %}
{%
  set low_batteries = states.sensor
    | selectattr('attributes.excludeFromMonitor', 'undefined')
    | rejectattr('attributes.device_class', 'undefined')
    | selectattr('attributes.device_class', 'eq', 'battery')
    | rejectattr('attributes.unit_of_measurement', 'undefined')
    | rejectattr('attributes.unit_of_measurement', 'ne', '%')
    | rejectattr('state', 'in', ['unavailable', 'unknown', 'none', 'Full'])
    | map(attribute='state')
    | map('float')
    | select('lt', level)
    | list
%}
{{ low_batteries }}

Thanks for your help!

1 Like

Hi,

great statemant. Do you have a idea to get the entity_id of the selected entities? I want to create a group, and your Selects give me the batterie states as a list.

Thanks, Steffen