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.

1 Like

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.