ZWave-JS missing "last received" attribute?

Hello, I previously used the deprecated ZWave integration in my Home Assistant Core install with an automation to monitor the time period since last activity of my battery supplied modules and notify if a module didn’t talk to the controller for too long.
I found the battery level info sent by the modules aren’t reliable, I had a module that worked for months with 0% ; and recently a module stopped working with battery readout of 100% while the battery voltage (without load) was 1.05V instead of 3.6V. So this automation is more reliable to notify me if a module didn’t wake up as expected.

alias: Zwave - Module ne répond plus
description: Envoie une notification si un module Zwave sur batterie ne répond plus.
trigger:
  - platform: time
    at: '19:00:00'
condition:
  - condition: or
    conditions:
      - condition: template
        value_template: >
          {%- set max_delay = 36000 -%} {% set ns = namespace(found=false) -%}
          {% for entity_id in states.group.zwave_activity.attributes.entity_id
          -%}
            {% set parts = entity_id.split('.') -%}
            {%- if (as_timestamp(now()) | int) - ((as_timestamp(strptime(states[parts[0]][parts[1]].attributes.receivedTS | string | truncate(19,True,'',0),'%Y-%m-%d %H:%M:%S:%f'))) | int) > 36000 -%}
              {% set ns.found = true -%}
            {% endif -%}
          {% endfor -%} {{ ns.found }}
action:
  - service: persistent_notification.create
    data:
      notification_id: >
        {%- set max_delay = 36000 -%} {%- for entity_id in
        states.group.zwave_activity.attributes.entity_id -%}
          {%- set parts = entity_id.split('.') -%}
          {%- if (as_timestamp(now()) | int) - ((as_timestamp(strptime(states[parts[0]][parts[1]].attributes.receivedTS | string | truncate(19,True,'',0),'%Y-%m-%d %H:%M:%S:%f'))) | int) > 36000 -%}
            {{ states[parts[0]][parts[1]].name }}
          {%- endif -%}
        {%- endfor -%}
      title: Module Zwave ne répond plus
      message: >
        {%- set max_delay = 36000 -%} {%- for entity_id in
        states.group.zwave_activity.attributes.entity_id -%}
          {%- set parts = entity_id.split('.') -%}
          {%- if (as_timestamp(now()) | int) - ((as_timestamp(strptime(states[parts[0]][parts[1]].attributes.receivedTS | string | truncate(19,True,'',0),'%Y-%m-%d %H:%M:%S:%f'))) | int) > 36000 -%}
            {{ states[parts[0]][parts[1]].name }} : dernière réponse le {{ states[parts[0]][parts[1]].attributes.receivedTS | string | truncate(19,True,'',0) }}.{{ '\n' }}
          {%- endif -%}
        {%- endfor -%}
mode: parallel
max: 10

The automation uses the devices specified in the group created with battery supplied ZWave modules :

zwave_activity:
  name: Zwave dernière activité
  entities:
    - zwave.fibaro_system_fgdw002_door_opening_sensor_2
    - zwave.fgdw002_fenetre_chambre_damis
    - zwave.fgdw002_fenetre_chambre_dapolline
    - zwave.fgdw002_fenetre_parentale
    - zwave.fibaro_system_fgk101_door_opening_sensor
    - zwave.popp_mold_detector_1
    - zwave.popp_mold_detector_2
    - zwave.popp_mold_chambre_d_amis
    - zwave.popp_mold_salle_de_bain

I switched to ZWave-JS a few weeks ago, and now the zwave.something entities doesn’t exist anymore, so I don’t know where to find the .attribute.receivedTS info from the ZWave devices.
Where could I find it ?
Thanks

There are no zwave entities in the zwave_js integration. Your use case is not supported currently.

You might be able to achieve the same using MQTT directly with zwavejs2mqtt. One of the MQTT topics provides a lastActive value and you could create an MQTT sensor out of that. This may or not be the exact same thing as OZW’s “receivedTS*” attribute.

When looking at one of my sensor’s entity states, I can see a time stamp, but I’m not sure it corresponds to the last data received, and I can’t find a way to extract it.

{{states.sensor.fgk101_thermometre_piscine_temperature}}

Returns
<template TemplateState(<state sensor.fgk101_thermometre_piscine_temperature=18.62; unit_of_measurement=°C, friendly_name=FGK101 Thermomètre Piscine: Water temperature, device_class=temperature @ 2021-05-12T20:23:39.379061+02:00>)>

You could try a workaround, by refreshing sensor values on a schedule and check the last updated attribute of the entity. For example, refresh the battery entities on a schedule, when those devices wake up, the battery value will be polled. That would update the battery sensor’s last updated attribute.

Or check the attribute of sensors that you expect to be updated regularly. Refreshing battery devices does reduce the battery itself to a degree.

{{states.sensor.fgk101_thermometre_piscine_temperature.last_updated}}

You might also look at last_changed. I’m not sure if one or both are always updated. zwave-js sensors have the “force update” flag turned on, which means even if a sensor updates with an unchanged value, it causes a state change to be recorded. On a few sensors I checked, last_changed and last_updated were the same values.

The last updated and last changed attributes behaves strangely. When one device ID updated, all devices timestamps are updated.

{{ states.binary_sensor.fgk101_thermometre_piscine_any.name }} : {{ as_timestamp(strptime(states.binary_sensor.fgk101_thermometre_piscine_any.last_updated, "")) | timestamp_local }}
{% for entity_id in states.group.zwave_battery_levels.attributes.entity_id -%}
  {%- set parts = entity_id.split('.') -%}
  {{ states[parts[0]][parts[1]].name }} : {{ as_timestamp(strptime(states[parts[0]][parts[1]].last_changed, "")) | timestamp_local }}.{{ '\n' }}
{%- endfor %}

FGK101 Thermomètre Piscine: Any : 2021-05-13 10:14:28
FGDW002 Porte de garage: Battery level : 2021-05-13 10:14:28.
FGK101 Thermomètre Piscine: Battery level : 2021-05-13 10:14:28.
FGDW002 Fenêtre Chambre d’amis: Battery level : 2021-05-13 10:14:28.
FGDW002 Fenêtre Chambre d’Apolline: Battery level : 2021-05-13 10:14:28.
FGDW002 Fenêtre Chambre parentale: Battery level : 2021-05-13 10:14:28.
Popp Mold - Chambre parentale - Battery level : 2021-05-13 10:14:28.
Popp Mold - Chambre Apolline - Battery level : 2021-05-13 10:14:28.
Popp Mold - Chambre d’amis : Battery Level : 2021-05-13 10:14:28.
popp_mold_salle_de_bain: Battery level : 2021-05-13 10:14:28.
Popp Mold Salon: Battery level : 2021-05-13 10:14:28.

@adrien.b did you resolve this somehow? I was also using “ReceivedTS” prior to migrating to zwave-js to setup alarms on “sensor not heard for too long” for critical sensors e.g. water flood detector.

Unfortunately, no. I couldn’t find a way to extract only the timestamps of the last received data.

Hello, I’ve seen that in the 2021.7 release of HA there is a new node_status entity for Z-Wave JS devices.
So I tried again to develop my template to detect if a battery powered node didn’t switched to active state for too long.

Please note that by default, the node_status entity is disabled for all the Z-Wave devices, so you first need to enable it to use it in HA.

Then, I made this template that you can adapt to your entities group name and test in the template development tool :

{%- set max_delay = 7200 -%}

{%- macro convert_time(time) -%}
  {%- set minutes = ((time % 3600) / 60) | int -%}
  {%- set hours = ((time % 86400) / 3600) | int -%}
  {%- set days = (time / 86400) | int -%}
  {%- set diff_time_str = "" -%}
  {%- if time < 60 -%}
    {% set diff_time_str = "Less than a minute" %}
  {%- else -%}
    {%- if days > 0 -%}
      {%- if days == 1 -%}
        {% set diff_time_str = diff_time_str ~ "1 day" %}
      {%- else -%}
        {% set diff_time_str = diff_time_str ~ days ~ " days" %}
      {%- endif -%}
    {%- endif -%}
    {%- if hours > 0 -%}
      {%- if days > 0 -%}
        {% set diff_time_str = diff_time_str ~ ', ' %}
      {%- endif -%}
      {%- if hours == 1 -%}
        {% set diff_time_str = diff_time_str ~ "1 hour" %}
      {%- else -%}
        {% set diff_time_str = diff_time_str ~ hours ~ " hours" %}
      {%- endif -%}
    {%- endif -%}
    {%- if minutes > 0 -%}
      {%- if days > 0 or hours > 0 -%}
        {% set diff_time_str = diff_time_str ~ ', ' %}
      {%- endif -%}
      {%- if minutes == 1 -%}
        {% set diff_time_str = diff_time_str ~ "1 minute" %}
      {%- else -%}
        {% set diff_time_str = diff_time_str ~ minutes ~ " minutes" %}
      {%- endif -%}
    {%- endif -%}
  {%- endif -%}
  {{ diff_time_str }}
{%- endmacro -%}


{% for entity_id in states.group.zwave_battery_levels.attributes.entity_id -%}
  {%- set parts = entity_id.split('.') -%}
    {{ states[parts[0]][parts[1]].name }} :{{ '\n' }}
    {%- set time_now = as_timestamp(now()) -%}
    {%- set last_status_change_str_local = as_timestamp(states[parts[0]][parts[1].replace('_battery_level', '_node_status')].last_changed) | timestamp_local -%}
    {%- set last_status_change_timestamp = as_timestamp(states[parts[0]][parts[1].replace('_battery_level', '_node_status')].last_changed) -%}
    {%- set delay = time_now - last_status_change_timestamp -%}
    {{ '\t' }}- Z-Wave State                  : {{states[parts[0]][parts[1].replace('_battery_level', '_node_status')].state}}
    {{ '\t' }}- Last status changed time      : {{ last_status_change_str_local }}
    {{ '\t' }}- Now timestamp seconds         : {{ time_now }}
    {{ '\t' }}- Last status timestamp seconds : {{ last_status_change_timestamp }}
    {{ '\t' }}- Difference seconds            : {{ delay }}
    {{ '\t' }}- Difference time               : {{ convert_time(delay) }}
    {{ '\t' }}- Maximum delay                 : {{ convert_time(max_delay) }}
    {{ '\t' }}- Is late                       : {{ delay > max_delay }}
    {{ '\n' }}
    {%- endfor %}

My battery powered devices group is defined as follows :

entity_id:
  - sensor.fgdw002_porte_de_garage_battery_level
  - sensor.fgk101_thermometre_piscine_battery_level
  - sensor.fgdw002_fenetre_chambre_damis_battery_level
  - sensor.fgdw002_fenetre_chambre_dapolline_battery_level
  - sensor.fgdw002_fenetre_chambre_parentale_battery_level
  - sensor.popp_mold_chambre_parentale_battery_level
  - sensor.popp_mold_chambre_apolline_battery_level
  - sensor.popp_mold_chambre_d_amis_battery_level
  - sensor.popp_mold_salle_de_bain_battery_level
  - sensor.popp_mold_salon_battery_level
order: 0
friendly_name: Zwave Niveaux Batteries

It gives me the following informations :

FGDW002 Porte de garage: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 17:51:10
- Now timestamp seconds : 1625765700.012623
- Last status timestamp seconds : 1625759470.477789
- Difference seconds : 6229.5348341465
- Difference time : 1 hour, 43 minutes
- Maximum delay : 2 hours
- Is late : False

FGK101 Thermomètre Piscine: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 19:22:32
- Now timestamp seconds : 1625765700.014772
- Last status timestamp seconds : 1625764952.997513
- Difference seconds : 747.0172588825226
- Difference time : 12 minutes
- Maximum delay : 2 hours
- Is late : False

FGDW002 Fenêtre Chambre d’amis: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 17:30:07
- Now timestamp seconds : 1625765700.016728
- Last status timestamp seconds : 1625758207.892783
- Difference seconds : 7492.1239449977875
- Difference time : 2 hours, 4 minutes
- Maximum delay : 2 hours
- Is late : True

FGDW002 Fenêtre Chambre d’Apolline: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 15:55:54
- Now timestamp seconds : 1625765700.01868
- Last status timestamp seconds : 1625752554.115724
- Difference seconds : 13145.902956008911
- Difference time : 3 hours, 39 minutes
- Maximum delay : 2 hours
- Is late : True

FGDW002 Fenêtre Chambre parentale: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 14:54:48
- Now timestamp seconds : 1625765700.020629
- Last status timestamp seconds : 1625748888.726924
- Difference seconds : 16811.293704986572
- Difference time : 4 hours, 40 minutes
- Maximum delay : 2 hours
- Is late : True

Popp Mold - Chambre parentale - Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 14:00:56
- Now timestamp seconds : 1625765700.022586
- Last status timestamp seconds : 1625745656.01279
- Difference seconds : 20044.009796142578
- Difference time : 5 hours, 34 minutes
- Maximum delay : 2 hours
- Is late : True

Popp Mold - Chambre Apolline - Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 15:58:57
- Now timestamp seconds : 1625765700.024662
- Last status timestamp seconds : 1625752737.568349
- Difference seconds : 12962.45631313324
- Difference time : 3 hours, 36 minutes
- Maximum delay : 2 hours
- Is late : True

Popp Mold - Chambre d’amis : Battery Level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 16:58:06
- Now timestamp seconds : 1625765700.026616
- Last status timestamp seconds : 1625756286.786591
- Difference seconds : 9413.240025043488
- Difference time : 2 hours, 36 minutes
- Maximum delay : 2 hours
- Is late : True

popp_mold_salle_de_bain: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 16:07:49
- Now timestamp seconds : 1625765700.028516
- Last status timestamp seconds : 1625753269.677768
- Difference seconds : 12430.350748062134
- Difference time : 3 hours, 27 minutes
- Maximum delay : 2 hours
- Is late : True

Popp Mold Salon: Battery level :
- Z-Wave State : asleep
- Last status changed time : 2021-07-08 18:16:30
- Now timestamp seconds : 1625765700.030434
- Last status timestamp seconds : 1625760990.668934
- Difference seconds : 4709.361499786377
- Difference time : 1 hour, 18 minutes
- Maximum delay : 2 hours
- Is late : False

And here is the final automation :

alias: Zwave - Module ne répond plus
description: Envoie une notification si un module Zwave sur batterie ne répond plus.
trigger:
  - platform: time
    at: '19:00:00'
condition:
  - condition: or
    conditions:
      - condition: template
        value_template: >
          {%- set max_delay = 36000 -%}
          {% set ns = namespace(found=false) -%}
          {% for entity_id in states.group.zwave_battery_levels.attributes.entity_id -%}
            {% set parts = entity_id.split('.') -%}
            {%- set time_now = as_timestamp(now()) -%}
            {%- set last_status_change_timestamp = as_timestamp(states[parts[0]][parts[1].replace('_battery_level', '_node_status')].last_changed) -%}
            {%- set delay = time_now - last_status_change_timestamp -%}    
            {%- if delay > max_delay -%}
              {% set ns.found = true -%}
            {% endif -%}
          {% endfor -%} {{ ns.found }}
action:
  - service: persistent_notification.create
    data:
      notification_id: >
        {%- set max_delay = 36000 -%}
        {%- for entity_id in states.group.zwave_battery_levels.attributes.entity_id -%}
          {%- set parts = entity_id.split('.') -%}
          {%- set time_now = as_timestamp(now()) -%}
          {%- set last_status_change_timestamp = as_timestamp(states[parts[0]][parts[1].replace('_battery_level', '_node_status')].last_changed) -%}
          {%- set delay = time_now - last_status_change_timestamp -%}
          {%- if delay > max_delay -%}
            {{ states[parts[0]][parts[1]].name.replace(': Battery level', '') }}
          {%- endif -%}
        {%- endfor -%}
      title: Module Zwave ne répond plus
      message: >
        {%- set max_delay = 36000 -%}
        {%- for entity_id in states.group.zwave_battery_levels.attributes.entity_id -%}
          {%- set parts = entity_id.split('.') -%}
          {%- set time_now = as_timestamp(now()) -%}
          {%- set last_status_change_timestamp = as_timestamp(states[parts[0]][parts[1].replace('_battery_level', '_node_status')].last_changed) -%}
          {%- set delay = time_now - last_status_change_timestamp -%}    
          {%- if delay > max_delay -%}
            {{ states[parts[0]][parts[1]].name.replace(': Battery level', '') }} : dernière réponse le {{ last_status_change_timestamp | timestamp_local }}.{{ '\n\n' }}
          {%- endif -%}
        {%- endfor -%}
mode: parallel
max: 10

2 Likes

thanks @adrien.b for sharing this. I’ve setup a simple report based on your idea above, looking at the last_updated attribute of the *_node_status entities, and then build a quick report with the auto-entities card. Time will tell if it works but defintely the last_updated property is not showing changes every few seconds (for the mains-powered zwave devices) as the ReceivedTS property used to show.

Based on my early tinkering with node_status/last_updated, for battery-powered devices, it indeed tracks sensor updates transmitted (e.g. I have a battery sensor transmitting temperature every two hours, I can see that last_updated is always < 2hours), but I am not sure if it will be useful for sensors that have nothing to transmit for days but still “wake up” every 12 hours, like typical PIRs.

I confirm the last updated status is set both for sensor’s data sent and simple wake up to ping the controller.
I have a rain sensor that send no data when it’s dry, but is set to wake up every 10 hours, and by setting the timeout to 12 hours in my automation, it never trigger the notifications, while it does when setting timeout to 9 hours.

1 Like

I have also noticed that battery levels reported by zwave devices is often wrong. Keeping track of the last active time reported by zwavejs seems like the best workaround. I ended up writing a custom mqtt sensor that produces a markdown report and list of devices that are past a certain threshold or if the device uses a battery. Enjoy!

configuration.yaml

mqtt:
  sensor:
    - name: Inactive Zwave Battery Devices
      state_topic: "zwave/_CLIENTS/ZWAVE_GATEWAY-zwavejs2mqtt/api/getNodes"
      value_template: "{{ now() }}"
      json_attributes_topic: "zwave/_CLIENTS/ZWAVE_GATEWAY-zwavejs2mqtt/api/getNodes"
      json_attributes_template: |-
        {%- set setting_find_older_than = timedelta(days=2) %}
        {%- set settting_show_battery_only = true %}
        {%- set setting_none_timestamp = 9000000000000000 %}

        {%- set data = namespace(from_ha=[], node_info_list=[], markdown="") %}

        {# Get HA data about the zwave devices. #}
        {%- set device_ids = integration_entities('zwave_js') | map('device_id') | unique | reject('eq',None) | list %}
        {%- for device_id in device_ids %}
          {%- set node_id = device_attr(device_id, 'identifiers') | list | last | list | last | regex_replace("^[0-9]+-", "") | regex_replace("-.+", "") %}
          {%- set data.from_ha = data.from_ha + [namespace(
            device_id = device_id | string,
            node_id = node_id | string
          )] %}
        {%- endfor %}

        {%- for result in value_json.result %}
          {# Find the latest activitiy time. #}
          {%- set ns = namespace(times=[], device_id=None, has_battery=false) %}
          
          {%- if "values" in result %}
            {%- for key, value in result["values"].items() %}
              {%- if "commandClassName" in value and value["commandClassName"] is not none and value["commandClassName"] | lower == "battery" %}
                {%- set ns.has_battery = true %}
              {%- endif %}
            
              {%- if "lastUpdate" in value and value["lastUpdate"] is not none %}
                {%- set ns.times = ns.times + [value["lastUpdate"] / 1000] %}
              {%- endif %}
            {%- endfor %}
          {%- endif %}

          {%- if "lastActive" in result and result["lastActive"] is not none %}
            {%- set ns.times = ns.times + [result["lastActive"] / 1000] %}
          {%- endif %}

          {%- if ns.times | length == 0 %}
            {%- set ns.times = ns.times + [setting_none_timestamp] %}
          {%- endif %}

          {%- if ns.times | length > 0 and ((settting_show_battery_only is false) or (settting_show_battery_only and ns.has_battery)) %}
            {%- set last_activity_timestamp = ns.times | sort | last %}
            {%- set last_activity_dt = as_datetime(last_activity_timestamp | string) %}
            {%- if last_activity_timestamp == setting_none_timestamp or (last_activity_dt is not none and now() - last_activity_dt > setting_find_older_than) %}
              {%- set name = result.name | trim %}
              {%- set manufacturer = result.manufacturer | trim %}
              {%- set product_description = result.productDescription | trim %}
              {%- set location = result.loc | trim %}
            
              {# Find the HA device id. #}
              {%- for ha_item in data.from_ha %}
                {%- if ha_item.node_id|string == result.id|string %}
                  {%- set ns.device_id = ha_item.device_id %}
                {%- endif %}
              {%- endfor %}
            
              {%- set data.node_info_list = data.node_info_list + [{
                "id": result.id,
                "name": name if name | length > 0 else None,
                "manufacturer": manufacturer if manufacturer | length > 0 else None,
                "product_description": product_description if product_description | length > 0 else None,
                "location": location if location | length > 0 else None,
                "last_activity": last_activity_timestamp,
                "device_id": ns.device_id
              }] %}
            {%- endif %}
          {%- endif %}
        {%- endfor %}

        {# Create the output object and markdown format. #}
        {%- set data.markdown = data.markdown + "| Name | Location | Last Activity |\n" %}
        {%- set data.markdown = data.markdown + "| ---- | -------- | ------------- |\n" %}
        {%- if data.node_info_list | length > 0 %}
          {%- for node_info in data.node_info_list | sort(attribute="last_activity", reverse=True) %}
            {%- set name_value = node_info.product_description if node_info.name is none else node_info.manufacturer if node_info.name is none else node_info.name %}
            {%- set device_id = node_info.device_id %}
            {%- set activity_dt = as_datetime(node_info.last_activity | string) %}
          
            {%- set name_cell = name_value if device_id is none else "[" + name_value + "](/config/devices/device/" + device_id + ")" %}
            {%- set location_cell = node_info.location | default("Unknown") %}
            {%- set activity_cell = None if node_info.last_activity == setting_none_timestamp else relative_time(activity_dt) %}
          
            {%- set name_cell_value = "Unknown" if name_cell is none else name_cell %}
            {%- set location_cell_value = "Unknown" if location_cell is none else location_cell %}
            {%- set activity_cell_value = "Unknown" if activity_cell is none else activity_cell %}
          
            {%- set data.markdown = data.markdown + "| " + name_cell_value + " | " + location_cell_value + " | " + activity_cell_value + " |\n" %}
          {%- endfor %}
        {%- else %}
          {%- set data.markdown = data.markdown + "| No devices |\n" %}
        {%- endif %}

        {{ {"markdown": data.markdown, "node_info_list": data.node_info_list } | to_json }}

Automation to refresh the data occasionally:

alias: Reload zwave node info
description: >-
  Used by the custom mqtt sensor sensor.inactive_zwave_devices. See
  configuration yaml for details.
trigger:
  - platform: time
    at: "00:00:00"
  - platform: homeassistant
    event: start
condition: []
action:
  - service: mqtt.publish
    data:
      topic: zwave/_CLIENTS/ZWAVE_GATEWAY-zwavejs2mqtt/api/getNodes/set
      payload: "{ \"args\": [] }"
mode: single

this looks like exactly what I need - a way to pull in the “Last Active” value from Z-Wave JS UI. Does this require setting “Disable MQTT Gateway” to false on the Z-Wave JS UI Settings page? And then where is this markdown report presented - on a dashboard? Trying to get a feel for level of effort before I dive in… Thanks!