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*
0.3 - update for new HomeyScript - compatible now only with HomeyScript v. 3.5.1 and later
0.3b - fixed for THEN card (returning TEXT value), primary use-better use is for AND card though

// Version 0.3b 
// Script checks devices based on the last updated time against a threshold
// Focuses on temperature sensors and specified device classes

const NotReportingThreshold = 0.8; // 0.8 hours, which is 48 minutes
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

// Function to format date as "dd-mm-yyyy, hh:mm:ss"
function formatDate(date) {
    if (!date) return "Unknown"; // Return 'Unknown' if date is null or invalid

    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}`;
}

for (const device of Object.values(devices)) {
    // Exclude Virtual Devices and specific apps
    if (device.driverUri && device.driverUri.match('vdevice|nl.qluster-it.DeviceCapabilities|nl.fellownet.chronograph|net.i-dev.betterlogic|com.swttt.devicegroups|com.gruijter.callmebot|com.netscan')) continue; 

    // Include by regex device name, focusing on temperature-related devices
    if (!device.name || !device.name.match(/temperature/i)) continue;

    // Exclude devices based on specific name patterns (if necessary, uncomment and modify)
    // if (device.name.match(/Flood|TVOC|Netatmo Rain|Motion|Flora|Rear gate Vibration Sensor|Vibration Sensor Attic Doors/i)) continue;

    // Include devices based on device class
    if (!device.class || !device.class.match(/sensor|button|remote|socket|lights/i)) continue;

    let IsReporting = false;
    let lastUpdated = null;

    for (const capability of Object.values(device.capabilitiesObj)) {
        if (!capability.lastUpdated) continue; // Skip capabilities without lastUpdated info
        let timeSinceReport = Date.now() - new Date(capability.lastUpdated).getTime();
        if (timeSinceReport < thresholdInMillis) {
            IsReporting = true;
            break;
        }
        // Track the most recent update
        if (!lastUpdated || new Date(capability.lastUpdated) > new Date(lastUpdated)) {
            lastUpdated = new Date(capability.lastUpdated);
        }
    }

    if (!IsReporting) {
        let deviceInfo = `${device.name} (${formatDate(lastUpdated)} - ${device.class}) (NOK)`;
        DevicesNotReporting.push(deviceInfo);
        console.log('NOK:', deviceInfo);

        // Increment the count
        notReportingCount++;
    } else {
        let deviceInfo = `${device.name} (${formatDate(lastUpdated)} - ${device.class}) (OK)`;
        console.log('OK:', deviceInfo);
    }
}

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

// Define a return value
const myTag = `Not Reporting Count: ${notReportingCount}\nDevices Not Reporting:\n${DevicesNotReporting.join('\n')}`;

// Return the defined value
return myTag;



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.3 - updated for HomeyScript v. 3.5.1
Version 0.3b - fixed for THEN card (returning TEXT value), primary use-better use is for AND card though
Version 0.4 - added option to filter out routers and/or end devices

// Version 0.4
// 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

const includeEndDevices = true; // Option to include end devices
const includeRouters = true; // Option to include router devices

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(/dimmer|light|bulb|spot/i)) continue; // Exclude by regex device name

      // Include devices based on type
      if (!includeRouters && device.type.toLowerCase() === 'router') continue;
      if (!includeEndDevices && device.type.toLowerCase() === 'enddevice') continue;

      // 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();

      let deviceName = device.name + ' ' + lastSeenFormatted + ' (' + device.type.toLowerCase() + ')';
      if (timeSinceLastSeen >= thresholdInMillis) {
        deviceName += ' (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 += ' (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 based on selection)');
    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);

    // Define a return value
    const myTag = `Not Reporting Count: ${notReportingCount}\nDevices Not Reporting:\n${DevicesNotReporting.join('\n')}`;

    // Return the defined value
    return myTag;
  } 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

    // Define a return value for error state
    const myTag = `Error in retrieving ZigBee state`;

    // Return the defined value
    return myTag;
  }
}

// Execute the Zigbee check function
const myTag = await checkZigbeeLastSeen();

// Return the defined value
return myTag;


Example of 2 hours threshold (not 10) :

picture

5 Likes

how can write this script the timeline or telegram notification?