A script to check sensor last update

:+1:

<nitpicking mode>
If you like, you can use it without the homeyscript tag, with the THEN card “Run code with Argument and return Text-tag”.
You can adjust the threshold value a bit easier, or even use a variable for that,

.

change these lines:

//GetDevicesNotReporting with argument [time in seconds]
const NotReportingThreshold = args[0]; // Trigger when not reported in the last x secs

//await tag('DevicesNotReporting', DevicesNotReporting.join(',\n'));
return (DevicesNotReporting.join(',\n'));

Peter what about this one, seems to be working better for me … it checks now if ANY capability has been updated and seems the time-date error is gone for me and the results are much more relevant.

@JeeHaa maybe it will help also with lights problem you mentioned but not sure how it behaves if user actually change the capability himself (so not the device by user/app etc.)

Latest update - A script to check sensor last update - #25 by Sharkys

1 Like

I am pretty sure the returned values are correct, some devices just don’t update as frequently on Homey.

However, I found a workaround to “poll” Lights: just send an On/Off command that is identical to its current state + using a Try/Catch. Effectively nothing changes, but the lastUpdated time gets updated.

//GetDevicesNotReporting
let NotReportingThreshold = 60; 
if (args[0]){ let NotReportingThreshold = args[0] }
let DevicesNotReporting = [];

let devices = await Homey.devices.getDevices();
for (const device of Object.values(devices)) {

  // Filter devices to check
  //if (device.driverUri.match('vdevice')) continue; // Exclude Virtual Devices
  if (!device.name.match(/lamp|^pc|plug/i)) continue; // Include by device name
  //if (!device.class.match('light|socket')) continue; // Include device types
  //if (device.class.match('sensor|button|remote|socket')) continue; // Exclude device types
  //if (!device.name.match('Plug JH')) continue; // Include by device name

  // Any capability updated recently? Set LastSeen to most recent update.
  IsReporting = false
  LastSeen = 1000000
  for (const capabilityObj of Object.values(device.capabilitiesObj)) {
    let lastUpdated = new Date(capabilityObj.lastUpdated)
    let timeSinceReport = Math.floor((Date.now() - lastUpdated)/1000/60) // Minutes since last update
    if (timeSinceReport < LastSeen){LastSeen = timeSinceReport }
    //log('LastUpdated: '+ device.name + ' ' + capabilityObj.id + ' ' + timeSinceReport) + 'min')
    if (timeSinceReport < (NotReportingThreshold)) { IsReporting = true }
  }

  // Add to list if not reported 
  //DevicesNotReporting.push(device.name + ' [' + LastSeen + 'm]');
  if (!IsReporting) {
    // Try to send command to wake up device if supported
    if (device.class === 'light' || device.virtualClass === 'light') {
      try {
        log('WAKE: ' + device.name + ' - ' + device.class + ' - ' + device.virtualClass + ' [' + LastSeen + 'm]')
        await device.setCapabilityValue('onoff', device.capabilitiesObj['onoff'].value)
      } catch {
        DevicesNotReporting.push(device.name + ' [' + LastSeen + 'm]');
        log('FAIL WAKE: ' + device.name + ' - ' + device.class + ' - ' + device.virtualClass + ' [' + LastSeen + 'm]')
      }
    } else {
      DevicesNotReporting.push(device.name + ' [' + LastSeen + 'm]');
      log('NOK: ' + device.name + ' - ' + device.class + ' - ' + device.virtualClass + ' [' + LastSeen + 'm]')
    }
  } else {
    log('OK: ' + device.name + ' - ' + device.class + ' - ' + device.virtualClass + ' [' + LastSeen + 'm]')
  }
}

// Output for script AND card
await tag('DevicesNotReporting', DevicesNotReporting.join(', '));
return DevicesNotReporting.length != 0;
//return DevicesNotReporting


This works fine for disconnected lights ("Catch’ gets executed as expected), but unfortunately my Tuya Plugs give an unexpected result. If a plug is removed from the wall socket this command still succeeds after 5s, and the LastUpdated time even updates (reported to Athom). :frowning:

Can anyone test with their plugs if they throw an error as expected when disconnected from power?

2 Likes

This might help you in your endeavor - I’ve had good experience with the a script I found some time ago: https://homey.solweb.no/battery/battery (not mine).

1 Like

Updated script I posted above - added counter of non responding devices - eg, in case you would like to initiate some action based on number of not-responding devices - please note this is the version I’m using, so feel free to change relevant variables below the section // Filter devices to check

Changelog :
0.1 - update - converting notReportingCount to a number - if you used previous version of script, delete tag by await tag(‘notReportingCount’, null);*
0.2 - added filtering based on device app type - thanks @Peter_Kawa*

//version 0.2
const NotReportingThreshold = 0.8; // 0.8
const thresholdInMillis = NotReportingThreshold * 3600000; // convert hours to milliseconds

let DevicesNotReporting = [];
let devices = await Homey.devices.getDevices();
let notReportingCount = 0; // Variable to keep track of the count

for (const device of Object.values(devices)) {
// Exclude Virtual Devices and apps:
if (device.driverUri.match('vdevice|nl.qluster-it.DeviceCapabilities|nl.fellownet.chronograph|net.i-dev.betterlogic|com.swttt.devicegroups|com.gruijter.callmebot|com.netscan' )) continue; 
  // Filter devices to check
  if (!device.name.match(/temperature/i)) continue; // Include by regex device name
  //if (device.name.match(/Flood|TVOC|Netatmo Rain|Motion|Flora|Rear gate Vibration Sensor|Vibration Sensor Attic Doors/i)) continue; // Exclude devices with given name
  if (!device.class.match(/sensor|button|remote|socket|lights/i)) continue; // Include device types

  // Any capability updated recently?
  let IsReporting = false;
  for (const capability of Object.values(device.capabilitiesObj)) {
    if (!capability.lastUpdated) continue; // Skip NULL values
    let timeSinceReport = Date.now() - new Date(capability.lastUpdated);
    if (timeSinceReport < thresholdInMillis) {
      IsReporting = true;
      break;
    }
  }

  // Add to list if not reported 
  if (!IsReporting) {
    let lastUpdated = null;
    for (const capability of Object.values(device.capabilitiesObj)) {
      if (capability.lastUpdated && (!lastUpdated || new Date(capability.lastUpdated) > new Date(lastUpdated))) {
        lastUpdated = capability.lastUpdated;
      }
    }
    DevicesNotReporting.push(device.name + ' (' + (lastUpdated ? new Date(lastUpdated).toLocaleString('en-GB', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/[./]/g, '-') : "Unknown") + ')');
    console.log('NOK: ' + device.name + ' - ' + device.class + ' - Last updated: ' + (lastUpdated ? new Date(lastUpdated).toLocaleString('en-GB', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/[./]/g, '-') : "Unknown"));

    // Increment the count
    notReportingCount++;
  } else {
    let lastUpdated = null;
    for (const capability of Object.values(device.capabilitiesObj)) {
      if (capability.lastUpdated && (!lastUpdated || new Date(capability.lastUpdated) > new Date(lastUpdated))) {
        lastUpdated = capability.lastUpdated;
      }
    }
    console.log('OK: ' + device.name + ' - ' + device.class + ' - Last updated: ' + (lastUpdated ? new Date(lastUpdated).toLocaleString('en-GB', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/[./]/g, '-') : "Unknown"));
  }
}

// Convert notReportingCount to a number and update the variable - just leftover, shouldn't be required
notReportingCount = parseInt(notReportingCount, 10);

// Output for script AND card
await ([
  tag('InvalidatedDevices', DevicesNotReporting.join('\n')),
  tag('notReportingCount', notReportingCount) // Tag notReportingCount as a number
]);

// Return both values
return {
  notReportingCount: notReportingCount,
  DevicesNotReporting: DevicesNotReporting
};

2 Likes

I used this in HS to force a value to be considered numeric, not sure if it works inside the tag arguments

tag('notReportingCount', Number(notReportingCount)())
1 Like

Hi, how do you mean and do this?
Can I use it to pull more updates out of a sensor?
I have the aware humidity and temp sensor. It doesn’t update a lot. And am triggering my shower ventilator with this moist measurement. If it takes too long, it doesn’t work correctly.

No, but you can use it to check when a value has been updated and how long ago that was.

Sorry, in general You can NOT pull more updates from sensor - most radio protocols on bat-powered devices save power and switch radio off. That means, the device is not listening radio traffic unless “wake up periods” or if there is something important to say ( for example: “Hi! i detected water here!” )
Yes, of course You can play with reporting intervals and also with threshholds on sensor - but this may reduce baterry life ( if bat. powered device ).

From my personal experience - also attempted first to check room moisture. Nice to have, but to slow and data to disperced ( for my case there is difference in moisture during summer and winter - but not only ) So… i just bougth a water leakage sensor and put it under the shower. Not disturbing at all - a small box hanging directly on wall under the shower head, around 5 centimeters abowe the floor. NB! there may be smart to put behind little bit more 2-sided tape, because there may be water staying between wall and sensor ( which one holds alarm on). And actually i use also timer in central box - the water alarm switch vent on & starts 1.5h timer. If timer reaches back to zero, then vent is switched off.
Ok, there are more whistles also… “hardware” switch on also sets countdown… etc.

I ran your script and got this:
image

Is this checking all the zigbee devices, doesn’t matter what type it is?

It matters, you need to adjust conditions in the script… name and-or class (device type)

Ok. That will be quite a job :smiley:

Or simply comment out the first two IF, it will give you everything from the class

Comment out, you mean just dont write anything there?

Can you give an example. I am pretty rooky on this scripting :dizzy_face:

Simply take the script above and launch it - based on the output, adjust it as needed.

Eg. without modifying anything, this will give you state of temperature sensors only, which were not updated in last 0.8 hours.

By launching it from Adv. flow, you will also get Console output.
obrazek

Or if you put // in front of this :

 if (!device.name.match(/temperature/i)) continue; //

You will get all sensor|button|remote|socket|lights …

1 Like

Now something happened here, very good :slight_smile:

Thank you!

I have to ask, there is a lot of devices that appear that is actually not a sensor. But the device class, who defines that?

App tiles I have made from the app “Net Scan” appears as sensors.
Weather forecast app tiles are appearing as a sensor.

The developer, or the nature of device?

.
Many capabilities are sensors. NetScan f.i. uses the sensor alarm capability.


Sensors example

.

For the lines with the filters, I use these for example. Maybe it makes a bit more sense to you.

// Exclude Virtual Devices and apps:
if (device.driverUri.match('vdevice|nl.qluster-it.DeviceCapabilities|nl.fellownet.chronograph|net.i-dev.betterlogic|com.swttt.devicegroups|com.gruijter.callmebot|com.netscan' )) continue; 

// Exclude by (parts of) device name
if (device.name.match('^se|^pr|TST|z_|z |z. |IR|Arri|Rebo|rada|KNM|Timeline')) continue; 


1 Like

As there is for some time options to see Lastseen timestamp for Zigbee devices, here is the script for Zigbee devices only, which might be more accurate for Zigbee devices. Thanks @Caseda for your overview script :wink:

// Version 0.2
// Script checks LastSeen property of Zigbee device
// Using 2 hours default threshold
// Thanks Caseda for [Homey Pro] overview script, which this was originally inspired by
// Thanks to ChatGPT for consultations ;-)

const NotReportingThreshold = 2; // 2 hours
const thresholdInMillis = NotReportingThreshold * 3600000; // convert hours to milliseconds

let ZigbeeDevicesLastSeen = [];
let notReportingCount = 0;
let DevicesNotReporting = [];

// Function to format a date as "dd-mm-yyyy, hh:mm:ss"
function formatDate(date) {
  const day = date.getDate().toString().padStart(2, '0');
  const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Months are 0-indexed
  const year = date.getFullYear().toString();
  const hours = date.getHours().toString().padStart(2, '0');
  const minutes = date.getMinutes().toString().padStart(2, '0');
  const seconds = date.getSeconds().toString().padStart(2, '0');

  return `${day}-${month}-${year}, ${hours}:${minutes}:${seconds}`;
}

// Function to check last seen state of Zigbee devices
async function checkZigbeeLastSeen() {
  try {
    const result = await Homey.zigBee.getState();

    const okDevices = [];
    const nokDevices = [];
    const routerDevices = [];
    const endDevices = [];

    for (const device of Object.values(result.nodes)) {
      // Filter devices to check
      if (device.class && !device.class.match(/sensor|button|remote|socket|lights/i)) continue; // Include device types
      //if (!device.name.match(/temperature/i)) continue; // Include by regex device name
      if (device.name.match(/dimmer|light|bulb|spot/i)) continue; // Exclude by regex device name
      // Include last seen information
      const lastSeenDate = new Date(device.lastSeen);
      const lastSeenFormatted = formatDate(lastSeenDate);

      // Check against NotReportingThreshold
      const currentTime = Date.now();
      const timeSinceLastSeen = currentTime - lastSeenDate.getTime();

      if (timeSinceLastSeen >= thresholdInMillis) {
        deviceName = device.name + ' ' + lastSeenFormatted + ' (' + device.type.toLowerCase() + ') (NOK)';
        nokDevices.push(deviceName);
        notReportingCount++; // Increment the count of not reporting devices
        DevicesNotReporting.push(deviceName); // Add the device to the list of not reporting devices
      } else {
        deviceName = device.name + ' ' + lastSeenFormatted + ' (' + device.type.toLowerCase() + ') (OK)';
        okDevices.push(deviceName);
      }

      if (device.type.toLowerCase() === 'router') routerDevices.push(deviceName);
      if (device.type.toLowerCase() === 'enddevice') endDevices.push(deviceName);
    }

    // Log Zigbee devices and their types
    console.log(okDevices.length, 'OK Zigbee devices', '(' + routerDevices.length + ' Router, ' + endDevices.length + ' End device)');
    console.log(nokDevices.length, 'NOK Zigbee devices');
    console.log('---------------------------------------------');
    console.log('OK ZigBee device(s):');
    console.log(okDevices.join('\r\n'));
    console.log('---------------------------------------------');
    console.log('NOK ZigBee device(s):');
    console.log(nokDevices.join('\r\n'));
    console.log('---------------------------------------------');

    // Output for script AND card
    await tag('InvalidatedDevices', DevicesNotReporting.join('\n'));
    await tag('notReportingCount', notReportingCount);

    // Return both values
    return {
      notReportingCount: notReportingCount,
      DevicesNotReporting: DevicesNotReporting
    };
  } catch (error) {
    console.error('Failed: Getting ZigBee state', error);
    // Output for script AND card
    await tag('InvalidatedDevices', '');
    await tag('notReportingCount', -1); // Set to -1 to indicate an error

    // Return both values
    return {
      notReportingCount: -1,
      DevicesNotReporting: [],
    };
  }
}

// Execute the Zigbee check function
checkZigbeeLastSeen()
  .then((result) => {
    // You can access result.notReportingCount and result.DevicesNotReporting here
    console.log('Not Reporting Count:', result.notReportingCount);
    console.log('Devices Not Reporting:', result.DevicesNotReporting);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

// Explicitly return true
return true;

Example of 2 hours threshold (not 10) :

picture

5 Likes

how can write this script the timeline or telegram notification?