Script to check "last seen" zigbee devices?

Hi, I have multiple zigbee devices (xiaomi ones). Running zigbee2mqtt addon. I noticed that all zigbee devices send messages even where there is no action (like button press or water leak). Just as “I am still here”. Like for example those water leak sensors. They send roughly once a hour simple message water_leak:false. I would like to have some kind of alert when some device stops sending messages - because I can imagine the battery fails or the electronics fails. And with those water leak or smoke alarms you will have only false sense of security while in reality the sensor could be dead.

So does anybody have some script that checks when the device sent last message and if this time period is longer than X minutes it will issue some alert? Thanks for replies :slight_smile:

Maybe this functionality is already implemented in zigbee2mqttassistant?

Thanks for the tip - didn’t hear about the zigbee2mqttassistant. Looks great. But the function to make alerts is not implemented there…so I guess I have to make some script to compare “last_seen” to current time

Hi,

Did you have already such a script. I also want a last seen below my smoke if it is still there (ex: last seen:13min)

regards RIchard

As template condition:

    {%- macro GetDroppedZigbee() -%}
    {% for state in states.sensor -%}
      {%- if state.attributes.linkquality %}
        {%- if "linkquality" in state.name and  (as_timestamp(now()) - as_timestamp(state.last_updated) > (24 * 60 * 60) ) -%}
         X 
        {%- endif -%}
      {%- endif -%}
    {%- endfor %}
    {%- endmacro -%}

    {%- if GetDroppedZigbee()[0] == "X" -%}
    true
    {%- else -%}
    false
    {%- endif -%}

As notification:

message: >
  Some Zigbee devices haven't been seen lately... {% for state in states.sensor
  -%}
    {%- if state.attributes.linkquality %}
      {%- if "linkquality" in state.name and  (as_timestamp(now()) - as_timestamp(state.last_updated) > (24 * 60 * 60) ) %}
        {{ relative_time(state.last_updated) }} ago for {{ state.name | lower }}
      {%- endif -%}
    {%- endif -%}
  {%- endfor %}

I trigger it twice a day and get a push notification to my phone.

Note: credit goes to someone else in the community whose name slips my mind at the moment.

5 Likes

For a ‘last seen’ use:

  - entity: your entity
    secondary_info: last-changed

@Skye great piece of code. Was about to make a similar “call for help” topic with these assumptions:

  • I`m ok with triggering automation once in a day by time; - check
  • It should loop through all entities. - check
  • I would like to add condition that returns true only if there is more than 0 entities that have last_changed > X - check

So have changed every last_updated to last_changed in your code and I am lacking only one thing.
How to exclude multiple inside template so that I can add some device that I do not really care about (printer, SHP6 connected to ironing board etc.). Adding and not 'X' in 'state' is not good for keeping code clear. I do not have any idea how to pass by list inside if loop since it should be string.

Z2M already offers some functionality for this. Wired devices are pinged every x seconds as set in config under:

advanced:
  availability_timeout: 60

Battery devices are marked as unavailable after 25 hours. I’d like to see this added per device so we can customise it. There’s an issue which needs reopening here

I just have an automation in HA to send an alert sent when the device is marked unavailable by Z2M.

1 Like

I want to share my setup. I’m using Z2M and first I tried availability_timeout config. I didn’t like it because it generates additional network traffic and I have mostly battery powered devices which are not pinged. Also when Z2M mark device as unavailable then you are unable to control it any more via HA.

For me that was no go and I went for binary sensor which checks last seen field. For that I enabled last_seen in Z2M config:

advanced:
  last_seen: ISO_8601_local

and then I created binary_sensor template like this:

binary_sensor:
  - platform: template
    sensors: 
      z2m_health:
        friendly_name: 'Z2M Mesh health'
        value_template: >-
          {%- macro CheckDroppedZigbee() -%}
          {% for state in states.sensor -%}
            {%- if ("linkquality" in state.name and state_attr(state.entity_id, "last_seen") != None and (as_timestamp(now()) - as_timestamp(state_attr(state.entity_id, "last_seen")) > (24 * 60 * 60))) -%}
            {{ state.name }}
            {%- endif -%}
          {%- endfor %}
          {%- endmacro -%}
          {% set output = CheckDroppedZigbee() %}
          {% if output | trim == "" %}
          false
          {%- else -%}
          true
          {%- endif -%}
        attribute_templates:
          data: >-
            {%- macro GetDroppedZigbee() -%}
            {% for state in states.sensor -%}
              {%- if ("linkquality" in state.name and state_attr(state.entity_id, "last_seen") != None and (as_timestamp(now()) - as_timestamp(state_attr(state.entity_id, "last_seen")) > (24 * 60 * 60))) -%}
              {{ state.name | regex_replace(find=' linkquality', replace='', ignorecase=False) }} - {{ relative_time(strptime(state_attr(state.entity_id, "last_seen"), '%Y-%m-%dT%H:%M:%S%z')) }} ago {{- '\n' -}}
              {%- endif -%}
            {%- endfor %}
            {%- endmacro -%}
            {{ GetDroppedZigbee() }}

Screenshot from 2021-04-20 19-41-19

I wanted to store message into attributes so that I can use it in Lovelace also. I’m using only linkquality entities.

After that you have everything and you can use this binary sensor in automations like this:

alias: Zigbee unhealthy alert
description: ''
trigger:
  - platform: state
    entity_id: binary_sensor.z2m_health
    to: 'on'
condition: []
action:
  - data:
      message: |-
        ❌ Zigbee last seen ❌️:  
        {{state_attr('binary_sensor.z2m_health', 'data')}}
    service: notify.telegrambotme
mode: single

Screenshot from 2021-04-20 19-42-53

Thanks to @Skye and @Tinkerer for code which gave me idea and direction. :slight_smile:

4 Likes

Here is my fairly simple automation. Thanks to all above!

- alias: Zigbee Device Missing Alert
  id: zigbee_device_missing_alert
  trigger:
    - platform: time
      at: "07:00"
    - platform: time
      at: "16:00"
  condition:
    - condition: template
      value_template: > 
        {% set ns = namespace(break = false) %}
        {% for state in states -%}
          {%- if state.attributes.last_seen %}
            {%- if (as_timestamp(now()) - as_timestamp(state.attributes.last_seen) > (60 * 60 * 6) ) and ns.break == false %}
              {%- set ns.break = true %}
              true
            {%- endif -%}
          {%- endif -%}
        {%- endfor %} 
  action:
    - service: notify.mobile_app_Pixel_XL
      data:
        message: >
          Some Zigbee devices haven't been seen lately...
          {% for state in states -%}
            {%- if state.attributes.last_seen %}
              {%- if (as_timestamp(now()) - as_timestamp(state.attributes.last_seen) > (60 * 60 * 6) ) %}
                {{ ((as_timestamp(now()) - as_timestamp(state.attributes.last_seen)) / (3600)) | round(1) }} hours ago for {{ state.name }}
                
              {%- endif -%}
            {%- endif -%}
          {%- endfor %}
2 Likes

Thank you for sharing your automation script!
Sorry, i’m noob, i’m getting following result using your script:
Some Zigbee devices haven’t been seen lately…
8.8 hours ago for ikeae14bulbupdateavailable
8.8 hours ago for ikeae273updateavailable
8.8 hours ago for ikeae14bulb
8.8 hours ago for ikeae273
8.8 hours ago for ikeae14bulblinkquality
8.8 hours ago for ikeae14bulbpoweronbehavior
8.8 hours ago for ikeae14bulbupdatestate
8.8 hours ago for ikeae273linkquality
8.8 hours ago for ikeae273poweronbehavior
8.8 hours ago for ikeae273updatestate

Is it possible to fix this somehow? There is only 2 devices.

Try:

          {% for state in states -%}
            {%- if (state.attributes.last_seen and not (state.name | regex_search('updateavailable|linkquality|poweronbehavior|updatestate'))) %}
              {%- if (as_timestamp(now()) - as_timestamp(state.attributes.last_seen) > (60 * 60 * 6) ) %}
                {{ ((as_timestamp(now()) - as_timestamp(state.attributes.last_seen)) / (3600)) | round(1) }} hours ago for {{ state.name }}
                
              {%- endif -%}
            {%- endif -%}
          {%- endfor %}

Thank you! But now it’s give me zero result.

You will just have to experiment and change the words the regex is looking for to exclude the entries you don’t want. I could only guess at what they were based on your post, you need to look at the entity names. Hopefully you are using the template editor which will allow you to quickly test different ideas.

Thank you so much! Here is the correct code for me:

          {% for state in states -%}
            {%- if (state.attributes.last_seen and not (state.name | regex_search('update_available|linkquality|power_on_behavior|update_state'))) %}
              {%- if (as_timestamp(now()) - as_timestamp(state.attributes.last_seen) > (60 * 60 * 6) ) %}
                {{ ((as_timestamp(now()) - as_timestamp(state.attributes.last_seen)) / (3600)) | round(1) }} hours ago for {{ state.name }}
                
              {%- endif -%}
            {%- endif -%}
          {%- endfor %}

Now i receive:
Some Zigbee devices haven’t been seen lately…
11.0 hours ago for ikeae14bulb
11.0 hours ago for ikeae271
11.0 hours ago for ikeae273

That’s what i wanted! Thank you.

P.S.
It’s ignoring “_” symbol in device name. Is it possible to fix?

1 Like

Using the current dev branch of z2m, with commit c28731957ae109d88e28b2fcafa49d5cac4079f5
using this template (paste in a markdown card) shows the most recent seen and least recent seen.

{% set ns = namespace(found=false) %}
{% set ns.worst = 0 %}
{% set ns.best = 99999999 %}
{% for state in states if 'last_seen' in state.attributes -%}
{% set raw_secs = (as_timestamp(now()) - as_timestamp(state.attributes.last_seen)) | round(0) -%}
{% if raw_secs < ns.best %}
{% set ns.best = raw_secs %}
{% set ns.best_name = state.name %}
{% endif %}
{% if raw_secs > ns.worst %}
{% set ns.worst = raw_secs %}
{% set ns.worst_name = state.name %}
{% endif %}
{% endfor %}
{% set days = ns.best // (24*3600) -%}
{% set hours = (ns.best - 24*3600*days) // 3600 -%}
{% set minutes = (ns.best - 24*3600*days - 3600*hours) // 60 -%}
{% set seconds = (ns.best - 24*3600*days - 3600*hours - 60*minutes) -%}
{% if days > 0 -%}
{{ ns.best_name }}: Last seen {{ days }}{{ ((hours/24) | round(1) | string)[1:] }} days ago.
{% elif hours > 0 -%}
{{ ns.best_name }}: Last seen {{ hours }}{{ ((minutes/60) | round(1) | string)[1:] }} hours ago.
{% elif minutes > 0 -%}
{{ ns.best_name }}: Last seen {{ minutes }}{{ ((seconds/60) | round(1) | string)[1:] }} minutes ago.
{% elif seconds > 0 -%}
{{ ns.best_name }}: Last seen {{ seconds }} seconds ago.
{% endif -%}
{% set days = ns.worst // (24*3600) -%}
{% set hours = (ns.worst - 24*3600*days) // 3600 -%}
{% set minutes = (ns.worst - 24*3600*days - 3600*hours) // 60 -%}
{% set seconds = (ns.worst - 24*3600*days - 3600*hours - 60*minutes) -%}
{% if days > 0 -%}
{{ ns.worst_name }}: Last seen {{ days }}{{ ((hours/24) | round(1) | string)[1:] }} days ago.
{% elif hours > 0 -%}
{{ ns.worst_name }}: Last seen {{ hours }}{{ ((minutes/60) | round(1) | string)[1:] }} hours ago.
{% elif minutes > 0 -%}
{{ ns.worst_name }}: Last seen {{ minutes }}{{ ((seconds/60) | round(1) | string)[1:] }} minutes ago.
{% elif seconds > 0 -%}
{{ ns.worst_name }}: Last seen {{ seconds }} seconds ago.
{% endif -%}

More info here

1 Like

Use markdown, that is, asterisks (’*’) surrounding your text.

{{"*"}}{{ state.attributes.friendly_name }}{{"*: "}}

1 Like

Thank you! Its working for me!
I changed {{ state.name }} to {{"*"}}{{ state.name }}{{"*"}}