Updated Appliance Monitoring rule

With the upgrade to v5.1 I, like I assume most are doing, have been updating my javascript based rules. In doing so I came across 4 rules that I use to monitor appliances, and announce when they are finished. What started out years ago as 2 rules - one for our washer and one for the dryer, has grown to 4 nearly identical rules. I thought that surely, there is a more efficient, and elegant way to do this.

The first thing I noticed is that all have 2 things in common. They all monitor the power usage from a smart plug, and they all announce when the appliance is done, sending text notifications to my wife and I as well as announcing it on our Echo devices.

This is where I asked myself, ā€œwhat would Rich or Justin do?ā€ Aha, openhab-rules-tools. I created 2 helper files and saved them in $[CONF}/automations/js/node_modules/mytools. The first is applianceFinish.js. Here it is:

const { Gatekeeper } = require('openhab_rules_tools');

/**
 * Appliance Finish Detector
 * Optimized for openHAB 5.1
 */
function applianceFinishDetector({
  applianceId,
  powerItem,
  runningWatts,
  idleWatts,
  idleDuration = 'PT10M',
  onFinished
}) {
  const logPrefix = `[ApplianceFinish:${applianceId}]`;
  const item = items.getItem(powerItem);
  
  // OH 5.1: Use numericState for cleaner unit handling
  const power = item.numericState; 

  if (power === null) {
    console.warn(`${logPrefix} Item ${powerItem} has no numeric state. Skipping.`);
    return;
  }

  const gk = cache.private.get(`${applianceId}_finishGK`, () => {
    console.info(`${logPrefix} Initializing Gatekeeper`);
    return new Gatekeeper();
  });

  const wasRunningKey = `${applianceId}_wasRunning`;
  const idleArmedKey  = `${applianceId}_idleArmed`;

  const wasRunning = cache.private.get(wasRunningKey, () => false);
  const idleArmed  = cache.private.get(idleArmedKey, () => false);

  // 1. START/RUNNING DETECTION
  if (power >= runningWatts) {
    if (!wasRunning) console.info(`${logPrefix} Appliance started (Power: ${power}W)`);
    
    // Clear any pending "Finish" timers because we are actively running
    gk.cancelAll();
    cache.private.put(wasRunningKey, true);
    cache.private.put(idleArmedKey, false);
    return;
  }

  // 2. IDLE DETECTION (Only care if it was previously running)
  if (power < idleWatts && wasRunning && !idleArmed) {
    console.info(`${logPrefix} Power dropped to ${power}W. Arming finish timer (${idleDuration})`);
    
    cache.private.put(idleArmedKey, true);

    gk.addCommand(idleDuration, () => {
      // Re-check state after timer expired
      const currentPower = items.getItem(powerItem).numericState;

      if (currentPower < idleWatts) {
        console.info(`${logPrefix} FINISHED: Maintained idle for ${idleDuration}`);
        cache.private.put(wasRunningKey, false);
        cache.private.put(idleArmedKey, false);
        if (typeof onFinished === 'function') onFinished();
      } else {
        console.info(`${logPrefix} Finish aborted: Power rose to ${currentPower}W`);
        cache.private.put(idleArmedKey, false);
      }
    });
  }
}

module.exports = { applianceFinishDetector };

The second is notifications.js, here it is:

function notifyIfHome(recipients, message) {
  recipients.forEach(({ presenceItem, email }) => {
    if (items.getItem(presenceItem).state === 'ON') {
      actions.NotificationAction.sendNotification(email, message);
    }
  });
}

 // Announce a message on multiple Echos, but allows for conditional filtering logic.


function announceOnEchos(echoItems, message) {
  // Ensure we are working with an array
  const itemsToAnnounce = Array.isArray(echoItems) ? echoItems : [echoItems];

  itemsToAnnounce.forEach(itemName => {
    // Logic for the Bedroom specific restriction
    if (itemName === 'Echo_Bedroom_TTS') {
      const hour = time.ZonedDateTime.now().hour();
      if (hour < 8 || hour >= 20) {
        console.debug(`[Helper] Muting Bedroom Echo due to quiet hours (${hour}:00)`);
        return; // Skip this specific item
      }
    }

    const item = items.getItem(itemName);
    if (item) {
      item.sendCommand(message);
    }
  });
}

module.exports = {
  notifyIfHome,
  announceOnEchos
};


Finally, I put the recipients and Echo devices in a globalConfig.js file for central management. Here it is:


// /automation/js/node_modules/mytools/globalConfig.js

const Recipients = [
  { name: 'Chet', presenceItem: 'ChetPresence', email: ā€˜chet@email.com' },
  { name: 'Kat',  presenceItem: 'KatPresence',  email: 'kat@email.com }
];

const Echo_Devices = [
  'Echo_Living_Room_TTS',
  'Echo_Office_TTS',
  'Echo_Bedroom_TTS' // Our helper already handles the quiet hours for this!
];

module.exports = { Recipients, Echo_Devices };



    notifyIfHome(Recipients, messageEN);
  }
});

With those functions split off into helper scripts in a globally reachable area, my new rules look like this:

const { applianceFinishDetector } = require('mytools/applianceFinish');
const { notifyIfHome, announceOnEchos } = require('mytools/notifications');
const { Recipients, Echo_Devices } = require('mytools/globalConfig');

// Call the helper to detect when the dryer finishes
applianceFinishDetector({
  applianceId: 'dryer',           // Unique ID for the dryer
  powerItem: 'Dryer_Power',       // Your actual dryer power item
  runningWatts: 200,               // Adjust based on dryer idle vs running power
  idleWatts: 170,                   // Power threshold considered "idle"
  idleDuration: 'PT10m',          // Duration the dryer must stay idle before notifying
  onFinished: () => {
    // Messages for notification
    const messageEN = 'May I have your attention please. All passengers on flight Dryer number 8 can collect their laundry at carousel number 8!';
    const messageDE = 'Achtung bitte. Alle Passagiere des Fluges Dryer Nummer 8 kƶnnen ihre WƤsche am GepƤckband Nummer 8 abholen.!';

   announceOnEchos(Echo_Devices, messageDE);

    notifyIfHome(Recipients, messageEN);
  }
});

The only differences between them are the specific values of the applianceFinishDetector function, and the announcement. It’s pretty much a cookie cutter operation to add any other appliance rule.

Why do I send the notifications only to people who are home? If I’m not home I can’t act on the notification, so it doesn’t make sense to send it.

2 Likes

Great post! Thanks for posting. It’s a wonderful example of the power of creating libraries.

Or there’s what I actually did. :wink:

This is one of the use cases I created the Threshold Alert rule template to solve. That template handles any problem that fits the pattern ā€œwhen an item meets some condition for a certain amount of time do somethingā€.

But I don’t bring this up to say ā€œeveryone should use my templateā€ as much to point it that as of 5.0 it’s possible to create rule templates locally. So another approach you could make is create a rule template with parameters to choose the parts that are different (I.e the arguments to the library functions). See How and Why to Write a Rule Template (revisited) for details.

It’s another approach that can be very powerful that few use.

I like the Threshold Alert template, and have added it to my template library. However I didn’t use it in this particular case because I need two thresholds for my Appliance Monitoring rule, an active threshold to transition the appliance to the active phase, and an idle threshold to test for doneness. If the appliance’s power usage drops from the active threshold to anything below the idle threshold, a timer starts. If, after 10 minutes, the power usage is still below the idle threshold, the appliance is considered done/off. I do this to prevent false positives. Even when the appliance is done and sitting idle for hours, stray voltages cause a measurable difference in the power graph to trigger the rule.

I have already updated my appliance monitoring rule. Instead of having 4 rules, each monitoring a seperate appliance. I have created one rule to monitor everything. I have put the power usage items into a group. The rule is triggered when a member of the group changes. I have added metadata to each item in the group that defines the active threshold, idlethreshold, message, id, and label.

const { applianceFinishDetector } = require('mytools/applianceFinish');
const { notifyIfHome, announceOnEchos } = require('mytools/notifications');
const { Recipients, Echo_Devices } = require('mytools/globalConfig');

const triggerItem = items.getItem(event.itemName);
const config = triggerItem.getMetadata('appliance')?.configuration;

if (config) {
  applianceFinishDetector({
    applianceId: config.id,
    powerItem: triggerItem.name,
    runningWatts: parseFloat(config.run),
    idleWatts: parseFloat(config.idle),
    onFinished: () => {
      // Use the custom message from metadata, or a default if it's missing
      const customMsg = config.msg || `Der ${config.id} ist fertig!`;
      
      announceOnEchos(Echo_Devices, customMsg);
      notifyIfHome(Recipients, customMsg);
    }
  });
}

I would be willing to try to make this a template for the community. However, my rule uses 3 helper apps I have saved to the automation/js/node_modules folder, so I’m not sure how to manage that complication. Of course the easy answer is for would be users to install those helper files I guess.

You could implement this with two separate instances of the Threshold Alert rule. One to test for each condition.

Again, I’m not saying this is the only or even the best way to solve the problem. Just that it’s available.

Or just copy the parts of your libraries to be part of the template instead of being loaded from a library. Most of my templates require the user to install OHRT and I can say based on experience it’s a pain. So much so that I created a rule template to install the library. If you’ve only one template on the marketplace, I’d recommend against that and just put the code inline so the template is self-contained.

However, I suspect that most users will have their own announcement and notification code they would want to use so you probably would only need the first import to be added to the template. Call another script written by the user where they can implement their own announcement/notification code or use the script parameter to let them write their own code as a parameter.