Automation Trigger: list of devices

Please explain how that’s possible. Where are the identifiable entities within the binary_sensor’s template?

PS
I tested the suggested binary_sensor, using three MQTT Sensors reporting battery level. Publishing a payload value of less than 50 to their state_topic results in no state change for the binary_sensor. That’s an expected result because the binary_sensor’s template contains no identifiable entities (so no listeners can be created to monitor the binary_sensor).

Yeah, you’ll need to add a list of batteries to entity_id or provide a 1 time shot like @123 is saying. From what I remember, expand() on groups now adds the entity_id’s found to the ‘entity_id’ list. So in theory, you could just create a group of battery devices and use this template:

value_template: >
  {%- for s in expand('group.batteries') %}
  ... etc...

It is the slider…

Unfortunately, not yet. It was introduced in 0.110 but, after testing it, I discovered it failed to work. I reported it as an issue and the correction is pending. I don’t know if it will make it into 0.113.

Yes and that makes the following statement misleading given that the discussion is about the OP’s requirements and petro’s binary_sensor:

happy to report is does evaluate correctly, even without having to set an entity_id

Undoubtedly, your template includes the input_number. That’s fine and good but including the entities within the template is not how the OP wishes to have the sensor work.

I’ve combined the suggestions provided by Petro and Mariusthvdb then added a few of my own ideas to create the following sensor and automation. I’ve tested them and confirmed they work.

This Template Sensor’s state reports the number of sensors with low batteries. The names of the corresponding sensors are reported in its battery_low attribute and are stripped of the words “battery level” in order to make them more compact.

The Template Sensor is refreshed by sensor.date (in other words, once a day at midnight). If that is insufficient, there are other ways to make it refresh more frequently.

sensor:
- platform: template
  sensors:
    battery_alert:
      entity_id: sensor.date
      value_template: >
        {% set ns = namespace(below=[]) %}
        {% for s in states.sensor 
              if s.entity_id.endswith('battery_level') and s.state != 'unknown' and s.state|int < 50 %}
          {% set ns.below = ns.below + [ s ] %}
        {% endfor %}
        {{ ns.below | count }} 
      attribute_templates:
        battery_low: >
          {% set ns = namespace(below=[]) %}
          {% for s in states.sensor 
                if 'battery_level' in s.entity_id and s.state != 'unknown' and s.state|int < 50 %}
            {% set ns.below = ns.below + [ s.name[:-14] ~ ' (' ~ s.state ~ '%)'] %}
          {% endfor %}
          {{ ns.below | join(', ') }}

Here is a sample automation that triggers at 08:30. If sensor.battery_level is greater than 0 it proceeds to produce a notification containing the string in the battery_low attribute. It uses the quantity of low-battery sensors to determine if the message should use the word “is” or “are”.

Be advised that, if there are any low batteries, this automation will produce a notification every morning. The advantage is that if the following day an additional sensor battery becomes low, the notification will include it as well. You will be reminded every morning until the batteries are finally replaced (which, depending on your mood, can be perceived to be either an advantage or disadvantage).

- alias:  Report when batteries are low
  trigger:
    platform: time
    at: '08:30:00'
  condition:
    condition: template
    value_template: "{{states('sensor.battery_alert')|int > 0}}"
  action:
    service: notify.notify
    data_template:
      title: 'Low Battery Alert'
      message: >
        {% set phrase = 's are ' if states('sensor.battery_alert')|int > 1 else ' is ' %}
        The following sensor{{ phrase }} low: {{ state_attr('sensor.battery_alert', 'battery_low') }}

If you prefer to be notified the instant a battery becomes low, the following automation will do that. However, because of the way a Template Trigger works, it will only notify you once, the moment the battery_alert sensor reports more than 0. It won’t trigger again if a second or third sensor’s battery becomes low. Before it can trigger again, the battery_alert sensor must first return to 0.

- alias:  Instantly report when a battery is low
  trigger:
    platform: template
    value_template: "{{states('sensor.battery_alert')|int > 0}}"
  action:
    service: notify.notify
    data_template:
      title: 'Low Battery Alert'
      message: >
        {% set phrase = 's are ' if states('sensor.battery_alert')|int > 1 else ' is ' %}
        The following sensor{{ phrase }} low: {{ state_attr('sensor.battery_alert', 'battery_low') }}

Here’s an example of the output after changing:
service: notify.notify
to:
service: persistent_notification.create

Screenshot from 2020-07-17 12-41-20

2 Likes

I translated:

to:

binary_sensor:
  - platform: template
    sensors:
      battery_alert:
        friendly_name: Battery alert
        device_class: safety
        value_template: >
          {%- set ns = namespace(below=[]) %}
          {%- 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 %}
              {%- set ns.below = ns.below + [ s ] %}
            {%- endif %}
          {%- endfor %}
            {{ns.below|count > 0}}
        attribute_templates:
          Below: >
            {%- set ns = namespace(below=[]) %}
            {%- 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 %}
                {%- set ns.below = ns.below + [s.attributes.friendly_name + ' (' + s.state + '%)'] %}
              {%- endif %}
            {%- endfor %}
            {%- if ns.below|count == 0 %} All batteries are fine
            {%- elif ns.below|count == 1 %}
              {{ ns.below[0] }} is low!
            {%- elif ns.below|count == 2 %}
              {{ ns.below|join(' and ') }} are low!
            {%- else %}
              {{ns.below[:-1]|join(', ')}}, and {{ns.below[-1]}} are low!
            {%- endif %}

input_number:
  battery_alert_level:
    icon: mdi:battery-alert
    name: Battery alert level
    min: 0
    max: 100
    step: 5
    unit_of_measurement: '%'

Didn’t want to mislead at all…but yeah, was mistaken about the reason for the binary_sensor to update. realized the the moment I wrote the post. sorry for that.

yes, too bad we can’t create that group dynamically. Or could your appdeamon app to create the all-groups use

          {%- set ns = namespace(below=[]) %}
          {%- for s in states.sensor %}
            {%- if ('battery_level' in s.entity_id or 'motion_sensor_battery' in s.entity_id)
                 %}
              {%- set ns.below = ns.below + [ s.entity_id ] %}
            {%- endif %}
          {%- endfor %}
            {{ns.below}}

or maybe even better (though this also adds the comma to the last somehow):

          {%- 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 %}

to create the group, and then the binary_sensor could use that group to expand…

Would there happen to be an easy way to exclude any entities with iPhone, iPad, or iPod in the entity_id?

I personally recommend using the Battery Status package from @NotoriousBDG.

Found here: https://raw.githubusercontent.com/notoriousbdg/Home-AssistantConfig/master/packages/battery_alert.yaml

It’s been around for a while but it still works fairly well with a few minor changes.

Here is my battery alert package based on that code:

################################################
## Customize
################################################
homeassistant:
  customize:
    ################################################
    ## Disabled Battery Sensors
    ################################################
    sensor.mbr_motion_battery_level:
      battery_alert_disabled: true
    ################################################
    ## Node Anchors
    ################################################
    package.node_anchors:
      customize: &customize
        package: 'battery_alert'

    ################################################
    ## Group
    ################################################

    group.battery_status:
      <<: *customize
      friendly_name: "Battery Status"
      icon: mdi:battery-charging
      control: hidden

    ################################################
    ## Automation
    ################################################
    automation.battery_persistent_notification:
      <<: *customize
      friendly_name: "Battery Persistent Notification"
      icon: mdi:comment-alert-outline

    automation.battery_persistent_notification_clear:
      <<: *customize
      friendly_name: "Battery Persistent Notification Clear"
      icon: mdi:comment-remove-outline

    automation.battery_notification_default:
      <<: *customize
      friendly_name: "Battery Notification Default Format"
      icon: mdi:comment-alert-outline

    automation.battery_notification_slack:
      <<: *customize
      friendly_name: "Battery Notification Slack Format"
      icon: mdi:comment-alert-outline


################################################
## Input Boolean
################################################
input_boolean:
  low_batteries:
    name: Low Batteries
    icon: mdi:battery-alert

################################################
## Input Number
################################################
input_number:
  battery_alert_threshold_max:
    name: "Max Alert Threshold"
    icon: mdi:arrow-collapse-up
    mode: slider
    min: -1
    max: 100

  battery_alert_threshold_min:
    name: "Min Alert Threshold"
    icon: mdi:arrow-collapse-down
    mode: slider
    min: -1
    max: 100

################################################
## Input Select
################################################
input_select:
  notifier_format:
    name: Notifier Format
    options:
     - Default
     - Slack
    icon: mdi:comment-question-outline

################################################
## Input Text
################################################
input_text:
  notifier_name:
    name: Notifier Name
    mode: text

################################################
## Automation
################################################
automation:
- alias: battery_test_script_notification
  initial_state: 'on'
  trigger:
    - platform: time_pattern
      minutes: '/1'
      seconds: 00
  action:
    - service: script.battery_check

- alias: battery_persistent_notification
  initial_state: 'on'
  trigger:
    - platform: time_pattern
      minutes: '/15'
      seconds: 00
    - platform: state
      entity_id:
        - input_number.battery_alert_threshold_min
        - input_number.battery_alert_threshold_max
  action:
    - condition: template
      value_template: &low_battery_check >
        {% macro battery_level() %}
        {% for entity_id in states.group.battery_status.attributes.entity_id if (
          not is_state_attr(entity_id, 'battery_alert_disabled', true)
          and states(entity_id) is not none
          and (
            (
              (
                states(entity_id) is number
                or states(entity_id) | length == states(entity_id)| int | string | length
                or states(entity_id) | length == states(entity_id)| float | string | length
              )
              and states(entity_id) | int < states.input_number.battery_alert_threshold_max.state | int
              and states(entity_id) | int > states.input_number.battery_alert_threshold_min.state | int
            )
            or states(entity_id) | lower == 'low'
            or states(entity_id) | lower == 'unknown'
            or states(entity_id) | lower == 'unavailable'
          )
        ) -%}
          {{ state_attr(entity_id, "friendly_name") }} ({{ states(entity_id) }})
        {% endfor -%}
        {% endmacro %}
        {{ battery_level() | trim != "" }}
    - service: input_boolean.turn_on
      data:
        entity_id: input_boolean.low_batteries
    - service: persistent_notification.create
      data_template:
        title: "Low Battery Levels"
        notification_id: low_battery_alert
        message: &message >
          {% macro battery_level() %}
          {% for entity_id in states.group.battery_status.attributes.entity_id if (
            not is_state_attr(entity_id, 'battery_alert_disabled', true)
            and states(entity_id) is not none
            and (
              (
                (
                  states(entity_id) is number
                  or states(entity_id) | length == states(entity_id)| int | string | length
                  or states(entity_id) | length == states(entity_id)| float | string | length
                )
                and states(entity_id) | int < states.input_number.battery_alert_threshold_max.state | int
                and states(entity_id) | int > states.input_number.battery_alert_threshold_min.state | int
              )
              or states(entity_id) | lower == 'low'
              or states(entity_id) | lower == 'unknown'
              or states(entity_id) | lower == 'unavailable'
            )
          ) -%}
            {{ state_attr(entity_id, "friendly_name") }} ({{ states(entity_id) }})
          {% endfor -%}
          {% endmacro %}
          {{ battery_level() }}

- alias: battery_persistent_notification_clear
  initial_state: 'on'
  trigger:
    - platform: time_pattern
      minutes: '/15'
      seconds: 00
    - platform: state
      entity_id:
        - input_number.battery_alert_threshold_min
        - input_number.battery_alert_threshold_max
  action:
    - condition: template
      value_template: &low_battery_clear >
        {% macro battery_level() %}
        {% for entity_id in states.group.battery_status.attributes.entity_id if (
          not is_state_attr(entity_id, 'battery_alert_disabled', true)
          and states(entity_id) is not none
          and (
            (
              (
                states(entity_id) is number
                or states(entity_id) | length == states(entity_id)| int | string | length
                or states(entity_id) | length == states(entity_id)| float | string | length
              )
              and states(entity_id) | int < states.input_number.battery_alert_threshold_max.state | int
              and states(entity_id) | int > states.input_number.battery_alert_threshold_min.state | int
            )
            or states(entity_id) | lower == 'low'
            or states(entity_id) | lower == 'unknown'
            or states(entity_id) | lower == 'unavailable'
          )
        ) -%}
          {{ state_attr(entity_id, "friendly_name") }} ({{ states(entity_id) }})
        {% endfor -%}
        {% endmacro %}
        {{ battery_level() | trim == "" }}
    - service: input_boolean.turn_off
      data:
        entity_id: input_boolean.low_batteries
    - service: persistent_notification.dismiss
      data:
        notification_id: low_battery_alert

- alias: battery_notification_default
  initial_state: 'on'
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
    - platform: state
      entity_id:
        - input_number.battery_alert_threshold_min
        - input_number.battery_alert_threshold_max
  action:
    - condition: template
      value_template: *low_battery_check
    - condition: template
      value_template: "{{ states.input_select.notifier_format.state == 'Default' }}"
    - service_template: >
        {% if "notify." in states.input_text.notifier_name.state %}
          {{ states.input_text.notifier_name.state }}
        {% else %}
          notify.notify
        {% endif %}
      data_template:
        title: "Low Battery Levels"
        message: *message

- alias: battery_notification_slack
  initial_state: 'on'
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
    - platform: state
      entity_id:
        - input_number.battery_alert_threshold_min
        - input_number.battery_alert_threshold_max
  action:
    - condition: template
      value_template: *low_battery_check
    - condition: template
      value_template: "{{ states.input_select.notifier_format.state == 'Slack' }}"
    - service_template: >
        {% if states.input_text.notifier_name.state != "" %}
          {{ states.input_text.notifier_name.state }}
        {% else %}
          notify.notify
        {% endif %}
      data_template:
        message: "Low Battery Levels"
        data:
          attachments:
          - color: '#52c0f2'
            title: "These devices have low battery levels"
            text: *message

- alias: update_battery_status_group_members
  initial_state: 'on'
  trigger:
    - platform: time_pattern
      minutes: '/1'
      seconds: 00
  action:
    - service: group.set
      data_template:
        object_id: "battery_status"
        entities: >-
          {%- for item in states.sensor if (
            not is_state_attr(item.entity_id, 'hidden', true)
            and (
              is_state_attr(item.entity_id, 'device_class', 'battery')
              or 'battery' in item.attributes.icon | lower
              or (item.entity_id | lower).endswith('_bat')
              or (item.name | lower).endswith('_bat')
              ) or (
                (
                  'battery' in item.entity_id | lower
                  or 'battery' in item.name | lower
                ) and (
                  item.attributes.icon is not defined
                ) and (
                  not is_state_attr(item.entity_id, 'battery_alert_disabled', true)
                )
              )
            )
          -%}
            {{ item.entity_id }}{% if not loop.last %}, {% endif %}
          {%- endfor -%}

sensor:
  platform: template
  sensors:
    my_phone_life360_battery_level:
      friendly_name: My Phone Battery Level from Life360
      value_template: "{{ state_attr('device_tracker.life360_me', 'battery') | round(0) }}"
      unit_of_measurement: "%"
      device_class: battery
    wife_phone_life360_battery_level:
      friendly_name: Wife's Phone Battery Level from Life360
      value_template: "{{ state_attr('device_tracker.life360_wife', 'battery') | round(0) }}"
      unit_of_measurement: "%"
      device_class: battery

it will create a pop up notification on a low battery (below the threshold that you can set in a card that you create from the entities from code above) and you can have it send a notification to your notification service if you want ( it has several built-in you can use).

Here is the notification and status card I use:

ex1

and here is the lovelace card I made to interact with the entities of the package:

ex2

Jayzus. That’s a boatload of code to monitor and report low battery levels.

Sensor batteries last several months (some even longer) so it seems like overkill to monitor their levels every minute. Even if you check once a day, you’ve got days, or even weeks, of advance notice before they go dead. :man_shrugging:

2 Likes

If you check them every minute, you won’t have long before they’re completely flat…

Mommy, are we there yet ?
Mommy, are we there yet ?
Mommy, are we there yet ?
Mommy, are we there yet ?
Mommy, are we there yet ?
Mommy, are we there yet ?

:rofl:

2 Likes

ok, so I am officially confused (again). Had thought to go another route, and create a base template sensor:

sensor:
  - platform: template
    sensors:
      low_battery_level:
        friendly_name: Low battery level
        value_template: >
          {%- set ns = namespace(below=[]) %}
          {%- 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 %}
              {%- set ns.below = ns.below + [s] %}
            {%- endif %}
          {%- endfor %}
            {{ns.below|count}}
        attribute_templates:
          List: >
            {%- set ns = namespace(below=[]) %}
            {%- 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 %}
                {%- set ns.below = ns.below + [s.attributes.friendly_name] %}
              {%- endif %}
            {%- endfor %}
              {{ns.below}}

on which I would create the binary_sensor:

binary_sensor:
  - platform: template
    sensors:
      battery_alert:
        friendly_name: Battery alert
        device_class: safety
        value_template: >
          {{states('sensor.low_battery_level')|int > 0}}
        attribute_templates:
          Below: >
            {% set count_below = states('sensor.low_battery_level')|int %}
            {% set list_below = state_attr('sensor.low_battery_level','List') %}
            {%- if count_below == 0 %} All batteries are fine
            {%- elif count_below == 1 %}
              {{list_below[0]}} is low!
            {%- elif count_below == 2 %}
              {{ list_below|join(' and ') }} are low!
            {%- else %}
              {{list_below[:-1]}}, and {{list_below[-1]}} are low!
            {%- endif %}

this doesnt workout as I expected:

while, if I use the template in one:

all is alright.

Why is this? cant we iterate the template sensor state/attributes?

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 :wink:

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.