Howto create battery alert without creating a template for every device

Edit May 14th, 2019: The latest code and instructions are available on GitHub at:

Original post:

I have many battery powered devices that have various methods to track battery level. Some of my devices have a separate entity that just represents battery while most of them have a battery_level attribute. To make it even more complex, I have some devices that report an 0-100 integer and others that use strings (Low, Medium, Full, etc.).

I’ve been wanting a way to identify any batteries that need my attention with very low admin overhead. I know that templates can be used to create separate battery entities for each device that has a battery, but it’s way to easy to forget to create or update them. I’ve been able to cobble together an alert that so far seems to be working to identify batteries that need attention with no additional configuration required beyond this single alert. The alert below nags me twice daily when any device has battery level < %40 or “Low”.

I don’t like all of the repetition in the conditions, but that’s the only way I know of (so far) that works. If anyone has suggestions to optimize this alert, I’d be happy to hear them.

Expand to see the Battery Alert details...
- alias: 'Battery Alert'
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
  condition:
    condition: or
    conditions:
      - condition: template
        value_template: >-
          {%- set threshold = 40 -%}
          {%- for item in states.light if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown")) -%}
          {%- if loop.first -%}
          {{ true }}
          {%- endif -%}
          {%- endfor -%}
      - condition: template
        value_template: >-
          {%- set threshold = 40 -%}
          {%- for item in states.switch if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown")) -%}
          {%- if loop.first -%}
          {{ true }}
          {%- endif -%}
          {%- endfor -%}
      - condition: template
        value_template: >-
          {%- set threshold = 40 -%}
          {%- for item in states.sensor if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown")) -%}
          {%- if loop.first -%}
          {{ true }}
          {%- endif -%}
          {%- endfor -%}
      - condition: template
        value_template: >-
          {%- set threshold = 40 -%}
          {%- for item in states.zwave if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown")) -%}
          {%- if loop.first -%}
          {{ true }}
          {%- endif -%}
          {%- endfor -%}
      - condition: template
        value_template: >-
          {%- set threshold = 40 -%}
          {%- for item in states.lock if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown")) -%}
          {%- if loop.first -%}
          {{ true }}
          {%- endif -%}
          {%- endfor -%}
  action:
    - service: notify.slack_notify
      data_template:
        message: >-
          {%- set threshold = 40 -%}
          {%- set domains = [states.light, states.switch, states.sensor, states.zwave, states.lock ] -%}
          {%- for domain in domains -%}
          {%- if loop.first -%}
          The following devices have low battery levels:
          {%- endif -%}
          {%- for item in domain if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown")) -%}
          {%- if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) %}
          {{ item.name }} ({{ item.attributes['battery_level'] }}),
          {% endif -%}
          {%- if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}),
          {% endif -%}
          {%- endfor -%}
          {%- endfor -%}
54 Likes

You could iterate through all devices of a domain, here’s an example for Z-Wave. Poster mentions it not working correctly, but after a restart it did work as expected.

1 Like

My code above already does that for zwave, light, switch, sensor, and lock domains. It would be easy to add more, but I stopped there because those domains cover all my battery powered devices.

To remove the duplication, you can set an array of domains to iterate over and do it all in a single condition:

{%- set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] -%}
{%- for domain in domains -%}
{%- for item in states[domain] ...

or just simply check every single entity:

{%- for item in states ...
2 Likes

The issue I had with looping in the condition like you’re suggesting is I couldn’t get it to return a single true on the first match. The part I’m missing to make this method work is that I can’t figure out how to break out of the first loop when the second loop finds the first match. Breaking out of the second loop is easy with loop.first, but I don’t know how to break out of the first loop when that happens. No matter what I tried, the best I could get it to return was “true”, “truetrue”, “truetruetrue”, etc., which doesn’t work for conditions.

That’s what I tried first, and it definitely would have been the easiest option. My issue with that method is you can’t get to the battery_level attribute when you loop like that. Nearly every other attribute is available, except for battery_level. That’s the main reason why I ended up looping through the domains instead.

You can remove all of the duplication by using a template sensor to track all of the entities with a low battery:

sensor:
- platform: template
  sensors:
    low_battery:
      friendly_name: "Low Battery"
      value_template: >
        {%- set threshold = 40 -%}
        {%- set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] -%}
        {%- for domain in domains -%}
        {%- for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
            {{ item.attributes.friendly_name }}{%- if not loop.last %}, {% endif -%}
        {%- endfor -%}
        {%- endfor -%}

Then use that value in your automation:

- alias: 'Battery Alert'
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
  condition:
    condition: template
    value_template: "{% if states('sensor.low_battery') %}true{% endif %}"
  action:
    - service: persistent_notification.create
      data_template:
        title: Low Battery
        message: "{{ states('sensor.low_battery') }}"
        notification_id: low-battery-alert
28 Likes

Okay, this is absolutely beautiful. I tried playing around with it to see if I could figure it out on my own with no luck. So, how would one extract the friendly name for the sensor that falls below the threshold? Also, how often would this poll checking for battery levels?

Sorry, I completely misread your first post, I thought you were having problems getting it working.

1 Like

this is great thank you…

I updated my post. For friendly names, just use item.attributes.friendly_name instead of item.name.

I honestly don’t know how often it’ll run, probably every single event loop (i.e. a lot). I don’t know how to reduce that without either specifying every entity to track in the template sensor or duplicating the template in both the condition and action. Maybe we can get the template sensor updated to allow a config option that specifies how often it should update.

1 Like

Here’s a bit of a hack to configure how often it updates:

First, you need to create some entity that will change states to trigger the update. I used an input_boolean:

input_boolean:
  update_low_battery:

Then set the template sensor to only update when that entity changes states:

sensor:
- platform: template
  sensors:
    low_battery:
      ....(see previous comment)...
      entity_id:
      - input_boolean.update_low_battery

Finally, add an automation to toggle the state of the entity whenever you want the template sensor to update:

- alias: Update Low Battery
  trigger:
  - platform: time
    minutes: '/30'
  action:
  - alias: Update Low Battery
    service: input_boolean.toggle
    data:
      entity_id: input_boolean.update_low_battery

In this example, input_boolean.update_low_battery will toggle states every 30 minutes, which then triggers sensor.low_battery to update. You could also set it up to update just before your alert automation runs as well if you wanted. That way you only update the sensor right before the automation runs to check for low battery.

6 Likes

@tboyce1, thanks so much! That’s definitely a lot cleaner and I like having that persistent notification. I made a few (mostly cosmetic) tweaks to what you sent. The trigger on your “Update Low Battery” automation needs a seconds: 00 attribute to keep it from running every second when the minutes match. I changed the condition on the automation to prevent empty alerts when there are no batteries. Finally, I added the current battery level to the notification.

I’ve got a few more ideas that can make this better, but I wanted to share the latest code that’s working for me.

input_boolean:
  battery_status_update:
    name: Battery Status Update

sensor:
- platform: template
  sensors:
    battery_status:
      friendly_name: "Battery Status"
      entity_id:
      - input_boolean.battery_status_update
      value_template: >
        {%- set threshold = 40 -%}
        {%- set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] -%}
        {%- for domain in domains -%}
        {%- for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
        {%- if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
        {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
        {%- if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
        {{ item.name }} ({{ item.state }}){%- if not loop.last %}, {% endif -%} {% endif -%}
        {%- endfor -%}
        {%- endfor -%}

automation:
- alias: Battery Status Update
  trigger:
  - platform: time
    minutes: '/5'
    seconds: 00
  action:
  - alias: Battery Status Update
    service: input_boolean.toggle
    data:
      entity_id: input_boolean.battery_status_update

- alias: 'Battery Alert'
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
  condition:
    condition: template
    value_template: "{% if states('sensor.battery_status') %}{{ true }}{% else %}{{ false }}{% endif %}"
  action:
    - service: persistent_notification.create
      data_template:
        title: Low Battery levels
        message: "{{ states('sensor.battery_status') }}"
        notification_id: low-battery-alert
    - service: notify.slack_notify
      data_template:
        message: >-
          These devices have low battery levels: {{ states('sensor.battery_status') }}
16 Likes

This is a great idea!

I took the last change from @NotoriousBDG and converted it into a package that can pretty much just be dropped in and will work. Just update the notification to your preference.

https://hastebin.com/ligoxabema.coffeescript

4 Likes

Awesome @sjabby! You beat me to it. That’s what I was planning to attempt next :slight_smile:. I’ve never created a package before, so this saved me a lot of time.

Cleaned up even more with help from @skalavala !

Removed the need for input boolean and separate sensor. Put the template code within the automation itself.


################################################################
## Packages / Battery levels
################################################################

################################################
## Customize
################################################

homeassistant:
  customize:
    ################################################
    ## Node Anchors
    ################################################

    package.node_anchors:
      customize: &customize
        package: 'battery_alert'

      expose: &expose
        <<: *customize
        haaska_hidden: false
        homebridge_hidden: false

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

    group.battery_alert:
      <<: *customize
      friendly_name: "Battery Alert"
      icon: mdi:steam

    ################################################
    ## Automation
    ################################################

    automation.battery_alert:
      <<: *customize
      friendly_name: "Battery Alert"

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

group:
  battery_alert:
    control: hidden
    entities:
      - automation.battery_alert

################################################
## Automation
################################################

automation:
- alias: battery_alert
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
  condition:
    - condition: template
      value_template: >
          {% macro battery_level() %}
          {%- set threshold = 40 -%}
          {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
          {% for domain in domains -%}
          {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
          {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
          {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
          {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
          {% endfor %}
          {%- endfor %}
          {% endmacro %}
          {{ battery_level() |trim != "" }}
  action:
    - service: persistent_notification.create
      data_template:
        title: Low Battery levels
        notification_id: low-battery-alert
        message: >
          {% macro battery_level() %}
          {%- set threshold = 40 -%}
          {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
          {% for domain in domains -%}
          {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
          {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
          {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
          {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
          {% endfor %}
          {%- endfor %}
          {% endmacro %}
          {{ battery_level() }}
    - service: notify.pushover
      data_template:
        title: "Battery status"
        message: >
          {% macro battery_level() %}
          {%- set threshold = 40 -%}
          {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
          {% for domain in domains -%}
          {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
          {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
          {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
          {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
          {% endfor %}
          {%- endfor %}
          {% endmacro %}
          {{ battery_level()}}


14 Likes

Very neat, great work, time to give it a test :slight_smile:

@sjabby can you help me understand what the Node Anchors do? I have never seen that before and not sure what homebridge is.

Node Anchors is used to simplify your configurations and avoid repetitions of settings. So instead of repeting all settings for each entity you just add an anchor to the setting. Example:

Without anchors:

################################################
## Without Node Anchors and Merge Key Tags
################################################

homeassistant:
  customize:
    light.back_porch:
      friendly_name: "Back Porch"
      package: 'philips_hue'

    light.chandelier:
      friendly_name: "Chandelier"
      package: 'philips_hue'

    light.dining_room:
      friendly_name: "Dining Room"
      package: 'philips_hue'

    light.entry_lamp:
      friendly_name: "Entry Lamp"
      package: 'philips_hue'

    light.front_porch:
      friendly_name: "Front Porch"
      package: 'philips_hue'

The same config with anchors:

################################################
## With Node Anchors and Merge Key Tags
################################################

homeassistant:
  customize:
    package.node_anchors: # This is just a dummy entry
      customize: &customize # Also a dummy entry that allows us to define the node anchor
        package: 'philips_hue'

    light.back_porch:
      <<: *customize # This merges the keys/values from "&customize"
      friendly_name: "Back Porch"

    light.chandelier:
      <<: *customize
      friendly_name: "Chandelier"

    light.dining_room:
      <<: *customize
      friendly_name: "Dining Room"

    light.entry_lamp:
      <<: *customize
      friendly_name: "Entry Lamp"

    light.front_porch:
      <<: *customize
      friendly_name: "Front Porch"

Full example from @dale3h: https://github.com/dale3h/homeassistant-config/blob/master/examples/yaml_anchors.yaml

5 Likes

That’s perfect! Thanks @sjabby, @skalavala, and @tboyce1 for all your help.

Here’s an update to the package with an alert that automatically clears the persistent notification when the low batteries are fixed.

################################################################
## Packages / Battery levels
################################################################

################################################
## Customize
################################################

homeassistant:
  customize:
    ################################################
    ## Node Anchors
    ################################################

    package.node_anchors:
      customize: &customize
        package: 'battery_alert'

      expose: &expose
        <<: *customize
        haaska_hidden: false
        homebridge_hidden: false

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

    group.battery_alert:
      <<: *customize
      friendly_name: "Battery Alert"
      icon: mdi:steam

    ################################################
    ## Automation
    ################################################

    automation.battery_alert:
      <<: *customize
      friendly_name: "Battery Alert"

    automation.battery_alert_clear:
      <<: *customize
      friendly_name: "Battery Alert Clear"

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

group:
  battery_alert:
    control: hidden
    entities:
      - automation.battery_alert
      - automation.battery_alert_clear

################################################
## Automation
################################################

automation:
- alias: battery_alert
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '18:00:00'
  condition:
    - condition: template
      value_template: >
          {% macro battery_level() %}
          {%- set threshold = 40 -%}
          {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
          {% for domain in domains -%}
          {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
          {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
          {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
          {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
          {% endfor %}
          {%- endfor %}
          {% endmacro %}
          {{ battery_level() |trim != "" }}
  action:
    - service: persistent_notification.create
      data_template:
        title: "Low Battery levels"
        notification_id: low-battery-alert
        message: >
          {% macro battery_level() %}
          {%- set threshold = 40 -%}
          {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
          {% for domain in domains -%}
          {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
          {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
          {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
          {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
          {% endfor %}
          {%- endfor %}
          {% endmacro %}
          {{ battery_level() }}
    - service: notify.slack_notify
      data_template:
        message: "Low Battery Levels"
        data:
          attachments:
          - color: '#52c0f2'
            title: "These devices have low battery levels"
            text: >
              {% macro battery_level() %}
              {%- set threshold = 40 -%}
              {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
              {% for domain in domains -%}
              {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
              {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
              {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
              {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
              {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
              {% endfor %}
              {%- endfor %}
              {% endmacro %}
              {{ battery_level() }}

- alias: battery_alert_clear
  trigger:
    - platform: time
      minutes: '/30'
      seconds: 00
  condition:
    - condition: template
      value_template: >
          {% macro battery_level() %}
          {%- set threshold = 40 -%}
          {% set domains = ['light', 'switch', 'sensor', 'zwave', 'lock'] %}
          {% for domain in domains -%}
          {% for item in states[domain] if ((item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) or ("battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown"))) -%}
          {% if (item.attributes.battery_level is defined and item.attributes['battery_level'] | int < threshold) -%}
          {{ item.name }} ({{ item.attributes['battery_level'] }}){%- if not loop.last %}, {% endif -%}{% endif -%}
          {% if "battery" in item.name | lower and ((item.state | int < threshold and item.state|int != 0) or item.state | lower == "low" or item.state | lower == "unknown") -%}
          {{ item.name }} ({{ item.state }}){% if not loop.last %}, {%- endif %} {% endif -%}
          {% endfor %}
          {%- endfor %}
          {% endmacro %}
          {{ battery_level() |trim == "" }}
  action:
    - service: persistent_notification.dismiss
      data:
        notification_id: low-battery-alert
8 Likes