Sensor Battery Notification

There are numerous Battery Level Notification solutions here, but most tend to rely on the sensor (or battery-powered device) accurately reporting battery levels. I have many battery-powered motion and multi sensors that I’ve had operational for more than a year, and most have now had their batteries replaced. I have the following Lovelace Card that shows the battery_level of all these devices.

I thought this was a good solution initially, but in only two cases (out of 11 so far) did this Card show anything less than 30% when a battery was clearly dead. The values shown on the Card for the two that didn’t show 0%, ranged from 30% to 90%!

The way I discovered these sensors were dead was that I noticed the temperature graph became suspiciously FLAT. I realized then I needed another way to determine battery failure in my sensors, and came up with a rather simple automation that seems to work well so far. It is binary, and could be enhanced to include notifications with the device name, etc., to suit various needs. But for me, this is more than sufficient. This doesn’t work as well for things like door/window sensors, unless there are some regular updates made by these devices that you can monitor. In my case, I use all my door sensors to control lights and chimes, so I just notice when they stop working.

The following automation works by monitoring the changes in the temperature reported by the sensors. I have my sensors set to report often at < 1 deg F variations. If the temperature doesn’t update over a given period, I set a boolean that turns an icon red on my status Card in Lovelace. I also log the entity in Logbook, which I can check to find the battery that needs replacing. For a couple of outdoor motion sensors that don’t report temperature, but DO report luminance, I monitor that instead of temperature. It really doesn’t matter what you monitor, as long as it changes fairly frequently. For the luminance and a couple of sensors that are in enclosed spaces, I had to set higher duration thresholds, since they hold valid values longer. Basically, when it stops reporting, I assume the battery is dead. I use an input_number to enter the shorter duration so I can tweak it over time to eliminate false positives. Currently it is 3 hours.

  # Sensor Update Overdue (Check Battery)
  - id: sensor_update_overdue_atm
    alias: Sensor Update Overdue (ATM)
    mode: parallel
    trigger:
      # Temp Sensors
      - platform: state
        entity_id:
          - sensor.zooz_zse19_siren_temperature
          - sensor.zooz_zse40_multi_garage_entry_temperature
          - sensor.zooz_zse40_multi_garage_left_temperature
          - sensor.zooz_zse40_multi_garage_right_temperature
          - sensor.zooz_zse40_multi_stairs_temperature
          - sensor.zooz_zse40_multi_downstairs_temperature
          - sensor.zooz_zse40_multi_den_temperature
          - sensor.fibaro_eye_office_temperature
          - sensor.fibaro_eye_mbr_temperature
          - sensor.fibaro_eye_lr_temperature
          - sensor.fibaro_eye_foyer_temperature
        for:
          hours: "{{ states('input_number.time_till_sensor_overdue_in') | int }}"
      # Outside Motion Sensors (slower to change)
      - platform: state
        entity_id:
          - sensor.zooz_zse29_outdoor_motion_front_luminance
          - sensor.zooz_zse29_outdoor_motion_side_luminance
          - sensor.sensative_strips_comfort_temperature
          - sensor.zooz_zse40_multi_shed_temperature
          - sensor.zooz_zse40_multi_vault_temperature
        for:
          hours: 24
      # Does NOT Check EcoLink Door Sensors, Smoke Detectors, Flood Sensors, Remote Switch
    action:
      - service: logbook.log
        data:
          name: Sensor Update
          message: "{{ state_attr(trigger.entity_id,'friendly_name') }} update overdue."
      - service: homeassistant.turn_on
        data:
          entity_id: input_boolean.sensor_update_overdue_ib

Here’s the icon on my Status Card.

the issue you will have is that the “for:” option is inherently unreliable if you are active developer of your HA instance.

It gets reset every time you restart HA or reload automations.

and there is no (easy?) way to “look back” to see if the automation was supposed to run and was missed.

it would be OK as long as you do very little development work (but who doesn’t do that? :wink:) or you rarely restart HA. Just as long as you understand the limitations.

2 Likes

That is true, but that’s the beauty of it, really. It doesn’t have to look back far, at most 24 hours. In my case, if my sensors in the first block don’t change within 3 hours, then I’ve probably got a dead battery. that would happen as I sleep. The other block is 24 hours at most, and I can go days without restarting HA or reloading automations, even though I’m fairly active with updates and changes.

Although, I could make this a bit more complex and check the last_updated time of each entity against now() to check the duration and not use the for:, but that would only help with automation reloads. Restarts screw up everything state-wise. Why do last_updated get reset upon restart for entities that rely on “restore state from before restart” from the cache btw? Seems like that could be kept intact.

lot’s of things could be done if anyone decided to fix it.

The devs don’t seem to think there are any issues with things as they are relating to timer type functions. believe me I’ve tried to get it noticed. No dice.

1 Like

Correct; identifying stale sensors via their last_changed property has the advantage of surviving Reload Automations (but not a restart when last_changed is reset).

I provided examples of how to detect stale sensors here:

It’s also a bit less demanding because it doesn’t require a listener for each entity. It simply runs periodically and computes elapsed time.

The example checks all sensors (and/or binary_sensors) but it can easily handle a group of specific entities.

1 Like

Well, isn’t THAT both simple and elegant! There are too many “under-the-hood” things I don’t know about HA, like the “listeners for each entity” you mention, that make any attempts at optimizing my scripts rather futile. In the past, we just knew things like which C functions or language constructs were the most efficient, but I’m still a little lost with how HA really works. I just haven’t had the courage to dig into the code… primarily because I don’t know Python well enough (and I find it quite awkward).

Thanks for the head’s up. I may give this a try!

Oh, and can you tell me where the documentation is for the form of the selectattr() filter you used. I found one with examples with two parameters in the Jinja docs, but I’ve seen this very same 3-parameter example twice now and I want to know how you KNEW you could do this THIS way.

Honestly, I probably saw an example posted by someone else and ran with it. :man_shrugging:

The “listeners” term was picked up from discussions in the GitHub repo (for Core). I’ve never actually explored the source-code to see how the “listener” sausage is made; understanding its purpose is adequate for my needs.

Here is a good explanation of selectattr(). Now I can use this filter with confidence!

  1. It’s minimalistic as far as explanations go.
  2. It’s Ansible which is a different dialect and so the examples do a thing or two that can’t be done in Home Assistant’s implementation of Jinja2.

If you check the latest Jinja2 documentation, you’ll see it can take multiple arguments. However, the paucity of examples makes it challenging to understand the full extent of its applications.

Case in point, there are many interesting examples of filtering here but they’re Ansible so you’ll encounter things that look really useful but can’t be done in Home Assistant.

https://docs.ansible.com/ansible/latest/user_guide/complex_data_manipulation.html

I came across a dead battery and while looking for a solution I found this thread.

How well has this been working for you?

Have you updated your code or looked into creating a blueprint?

Hi, I have indeed updated the code, incorporating the coolness that @123 Taras posted above. It is working great! I can change the input_number that checks the shorter time checked for failure, which has to be customized based upon your own wake up or update schedules for your battery devices, and the lists grows as I do. I found 3 hours works well for me, but I have ALL of my battery devices waking up every 2 hours.

The key here is that I expect that these sensors (mostly temp or illuminance) will update after a given time. If you have your sensors set to update every 10 deg F, then they won’t update very often, and your “dead time” will have to be longer to prevent false alarms. I use my Temp sensors for heat control, and therefore have them updating in 1/3 deg F increments. For the 24 hour group of sensors, they aren’t as critical, so I have them report less often to conserve battery. These are marked as failed after 24 hours.

Also, this doesn’t really cover all of my battery devices. I have a tilt sensor and a few door/window sensors that aren’t tracked by this, but since those are used every day, and control chimes/sirens, I KNOW when those go dead. I did this because of my heat. I found some temp/multi sensors were going dead and the temp stopped changing, as did my active heating control for that zone. Not good. The others could probably be added with some additional logic, I suppose.

I’ve included the current code below. I created two groups for the two types of sensor schedules, which is what the template checks. I stood on the shoulders of lots of other cool people to get that template to work!

I’ll also post the YAML from the auto-entities Lovelace card I use to show which sensors have failed batteries (basically, the list created by the template). So my UI shows a red icon on the status page (triggered by the input_boolean set in the automation) and a list of dead battery devices on the Batteries page populated by a template in the card YAML. You could, in reality get by with just the card, but I wanted both, hence the automation.

Here’s the automation code:

  # Sensor Update Overdue (Check Battery)
  - id: sensor_update_overdue_atm
    alias: Sensor Update Overdue (ATM)
    mode: parallel
    trigger:
      - platform: state
        entity_id: input_number.time_till_sensor_overdue_in
      - platform: template
        value_template: >
          {{ expand('group.battery_sensor_overdue_normal_grp')
            | selectattr('last_changed', 'lt',now()-timedelta(hours=states("input_number.time_till_sensor_overdue_in") | int)) 
            | map(attribute='entity_id') | list  | join('\n')
            ~ expand('group.battery_sensor_overdue_extended_grp')
            | selectattr('last_changed', 'lt',now()-timedelta(hours=24 | int))
            | map(attribute='entity_id') | list  | join('\n')
          }}
    condition:
    action:
      choose:     
        conditions: >     # this was a tricky one to solve
          {{ expand('group.battery_sensor_overdue_normal_grp')
            | selectattr('last_changed', 'lt',now()-timedelta(hours=states("input_number.time_till_sensor_overdue_in") | int)) 
            | map(attribute='entity_id') | list  | join('\n')
            ~ expand('group.battery_sensor_overdue_extended_grp')
            | selectattr('last_changed', 'lt',now()-timedelta(hours=24 | int))
            | map(attribute='entity_id') | list  | join('\n')
            != ""
          }}
        sequence: 
          service: homeassistant.turn_on
          data:
            entity_id: input_boolean.sensor_update_overdue_ib
      default:
        service: homeassistant.turn_off
        data:
          entity_id: input_boolean.sensor_update_overdue_ib

And the Lovelace Card:

type: custom:auto-entities
card:
  type: entities
  title: Check Batteries
  show_header_toggle: true
  state_color: false
filter:
  template: |
    {{ expand('group.battery_sensor_overdue_normal_grp')
      | selectattr('last_changed', 'lt',now()-timedelta(hours=states("input_number.time_till_sensor_overdue_in") | int)) 
      | map(attribute='entity_id') | list  | join('\n')
      ~ expand('group.battery_sensor_overdue_extended_grp')
      | selectattr('last_changed', 'lt',now()-timedelta(hours=24 | int))
      | map(attribute='entity_id') | list  | join('\n')
    }}
show_empty: true
sort:
  method: none

which looks like the following when populated with a check time of 45 minutes (at 3 hours the list is empty):
image

Pretty simple, but it works for me!

Cheers