Some JS Scripting UI Rules Examples

Edit: see Some JS Scripting UI Rules Examples - #14 by rlkoshak for some more point examples.

I’ve managed to polish off my primary rules so they use JS Scripting and the openhab-js library it comes with. I’m posting them here just so people can have some working examples to look at and ask questions about. Be sure to refer to the JS Scripting addon docs for full documentation on the library: JavaScript Scripting - Automation | openHAB

Libraries

I’m trying to use the Node.js npm way of writing and using libraries. I’m mainly going from examples so I’m certain there’s lots I’m doing wrong. Anyway:

  1. create a folder under $OH_CONF/automation/js/node_modules
  2. run npm init and answer the questions
  3. create index.js where you will list your exports
  4. create your file(s) with your library functions, classes, etc.

But definitely go out and find a good tutorial.

An example of my personal index.js

module.exports = {
  get alerting() { return require('./alerting.js') },
  get utils() { return require('./utils.js') }
}

I’ve two files that are exported, one as alerting and the other as utils.

When adding new files, remember to update the index.js file.

I don’t explicitly require openhab in these and I probably should. They seem to work without doing so though.

**I’m not posting these libraries for your to just copy and use. They are intended to be examples to learn from. If you do want to use these libraries, wait a bit until I have them available in npm where they can just be installed.

personal

These are pretty basic and simple functions that get used in lots of my rules. Mail is not working right now so I’ve commented that out and am using the Notification actions instead for alerting.

alerts.js

exports.sendAlert = function(message, logger) {
  var logger = (logger) ? logger : log('sendAlert');
  logger.warn('ALERT: ' + message);
  actions.NotificationAction.sendBroadcastNotification(message, 'alarm', 'alert');
//  if(!actions.Things.getActions("mail", "mail:smtp:gmail").sendMail("email@server.com", "openHAB Alert", message)) {
//    logger.error("Failed to send email alert alert");
//  }
}

exports.sendInfo = function(message, logger) {
  var logger =  (logger) ? logger : log('sendInfo'); 
  logger.info('INFO: ' + message);
//  if(!actions.Things.getActions("mail", "mail:smtp:gmail")
//                    .sendMail("email@server.com", "openHAB Info", message)) {
//    logger.error("Failed to send email info alert");
//  }
}

exports.isNight = function() {
  const currToD = items.getItem('TimeOfDay').state;
  return currToD == 'NIGHT' || currToD == 'BED';
}

exports.isAway = function() {
  return exports.isNight() || items.getItem('Presence').state != 'ON';
}

exports.getNames = function(group, filterFunc) {
  return items.getItem(group.name || group).members
                             .filter(filterFunc)
                             .map(s => s.getMetadataValue('name') || s.label)
                             .join(', ');
}

Note that these are defined on exports so they get exposed when requires in the rule. You can also/alternatively list them (which you’ll see later).

utils.js

exports.hysteresis = function(curr, threshold, hyst) {
  var min = max - hyst;
  if(curr < min) return 'ON';
  else if(curr > max) return 'OFF';
  else return 'STAY';
}

I expect over time this file will grow.

openhab_rules_tools

I’ve not converted all of my openhab_rules_tools libraries to JS Scripting. Eventually I will and hope to move some of them into openhab-js so you just get them. In the mean time I hope to get them so they can be installed using npm which will make a slightly easier approach than git cloning and copying files around.

For full docs and usage examples see GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules..

timeUtils

const javaZDT = Java.type('java.time.ZonedDateTime');
const String = Java.type('java.lang.String');
const DateTimeType = Java.type('org.openhab.core.library.types.DateTimeType');
const DecimalType = Java.type('org.openhab.core.library.types.DecimalType');
const PercentType = Java.type('org.openhab.core.library.types.PercentType');
const QuantityType = Java.type('org.openhab.core.library.types.QuantityType');

const logger = log("rules_tools.timeUtils");

/**
 * @namespace timeUtils
 */

/**
 * Parses a duration string returning a js-joda Duration representing the 
 * duration. Supports the followiung units:
 *   - d days
 *   - h hours
 *   - m minutes
 *   - s seconds
 *   - s milliseconds
 * The unit is preceeded by an integer (decimals are not supported).
 * Examples:
 *   - 5d 2h 7s
 *   - 5m
 *   - 1h23m
 * 
 * @param {String} durationStr string representation of the duration
 * @returns {time.Duration} null if the string is not parsable
 */
const parseDuration = (durationStr) => {
  var regex = new RegExp(/[\d]+[d|h|m|s|z]/gi);
  var numMatches = 0;
  var part = null;

  var params = { "d": 0, "h": 0, "m":0, "s":0, "z":0 };
  while(null != (part=regex.exec(durationStr))) {
    logger.debug("Match = " + part[0]);
    numMatches++;

    var scale = part[0].slice(-1).toLowerCase();
    var value = Number(part[0].slice(0, part[0].length-1));
    params[scale] = value;
  }

  if(numMatches === 0){
    logger.warn("Could not parse any time information from '" + timeStr +"'. Examples of valid string: '8h', '2d8h5s200z', '3d 7m'.");
    return null;
  }
  else {
    logger.debug("Days = " + params["d"] + " hours = " + params["h"] + " minutes = " + params["m"] + " seconds = " + params["s"] + " msec = " + params["z"]);
    return time.Duration.ofDays(params["d"]).plusHours(params["h"]).plusMinutes(params["m"]).plusSeconds(params["s"]).plusMillis(params["z"]);
  }
}

/**
 * Adds the duration to now. If the duration is a String, call parseDuration
 * first. 
 * @param {time.Duration|String} duration
 * @returns {time.ZonedDateTime} the duration added to now, null if not a usable duration
 */
const durationToDateTime = (duration)=> {
  if(duration instanceof  time.Duration) {
      return time.ZonedDateTime.now().plus(duration);
  }
  else if(typeof duration === 'string' || duration instanceof String){
    return durationToDateTime(parseDuration(duration));
  } 
}

/**
 * Tests the passed in string to see if it conforms to the ISO8601 standard
 * @param {String} dtStr potential ISO8601 string
 * @returns {boolean} true if ISO8601 format
 */
const isISO8601 = (dtStr) => {
  var regex = new RegExp(/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$/);
  return regex.test(dtStr);    
}

/**
 * Converts a number of date time formats and duration formats to a time.ZonedDateTime
 * @param {time.ZonedDateTime|java.time.ZonedDateTime|String|number|bigint|DateTimeType|DecimalType|PercentType|QuantityType} when date time or duration to convert
 * @returns {time.ZonedDateTime} null if it cannot be converted
 */
const toDateTime = (when) => {
  var dt = null;

  if(!when) {
    logger.info("when is not defined: {}", when);
    // leave dt as null
  }
  else if(when instanceof time.ZonedDateTime) {
    logger.debug("Already ZonedDateTime " + when.toString());
    dt = when;
  }
  else if(when instanceof javaZDT) {
    logger.debug('Converting from Java ZonedDateTime');
    const rfcFormatter = time.DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSS[xxxx][xxxxx]");
    dt = time.ZonedDateTime.parse(when.toString(), rfcFormatter);
  }
  else if(when instanceof Date) {
    logger.debug('Converting from JS Date');
    const native = time.nativeJs(when);
    const instant = time.Instant.from(native);
    dt = time.ZonedDateTime.ofInstant(instant, time.ZoneId.SYSTEM);
  }
  else if(typeof when === 'string' || when instanceof String){
    if(isISO8601(when)){
      logger.debug("Converting ISO80601 local date time " + when);
      dt = time.ZonedDateTime.of(time.LocalDateTime.parse(when), time.ZoneId.systemDefault());
    }
    else {
      logger.debug("Converting duration " + when);
      dt = durationToDateTime(when);
    }
  }
  else if(typeof when === 'number' || typeof when === "bigint") {
    logger.debug("Converting number " + when);
    dt = time.ZonedDateTime.now().plus(when, time.ChronoUnit.MILLIS);
  }
  else if(when instanceof DateTimeType){
    logger.debug("Converting DateTimeType " + when.toString());
    dt = when.getZonedDateTime();
  }
  else if(when instanceof DecimalType || when instanceof PercentType || when instanceof QuantityType || when instanceof Number){
    logger.debug("Converting openHAB number type " + when.toString());
    dt = time.ZonedDateTime.now().plus(when.longValue(), ChronoUnit.MILLIS);
  }
  else {
    logger.warn("In toDateTime, cannot convert when, unknown or unsupported type: " + when);
  }

  return dt;
}

/**
 * Moves the date time to today's date. If pass a duration, converts it to a 
 * ZonedDateTime and then moves it to today's date.
 * @param {time.ZonedDateTime|java.time.ZonedDateTime|String|number|bigint|DateTimeType|DecimalType|PercentType|QuantityType} when date time or duration to move to today's date
 * @returns time.ZonedDateTime with today's date
 */
const toToday = (when) => {
  var now = time.ZonedDateTime.now();
  var dt = toDateTime(when);
  return dt.withYear(now.getYear())
           .withMonth(now.getMonth())
           .withDay(now.getDayOfMonth());
}

module.exports = {
  parseDuration,
  durationToDateTime,
  isISO8601,
  toDateTime,
  toToday
}

The toDateTime function is the most useful function. It’s a Swiss Army knife able to convert just about anything you can thing of to a joda-js ZonedDateTime including durations defined by a String like “3h2s”, numbers (which are assumed to be milliseconds from now), ISO8601 Strings, etc. I use the duration Strings almost exclusively in my rules.

The toToday function should handle DST change overs properly. It’s only tested twice a year so hard to be sure. :wink:

Notice how in this case, the functions are listed for export instead of defining them on exports.

timerMgr

This is almost a one stop shop for managing Timers. It implements all the book keeping involved when one has a rule that needs to keep track of lots of different timers.

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

/**
 * @namespace TimerMgr
 */

/**
 * Implements a manager for Timers with a simple interface. Once built, call
 * check to create a timer or to reschedule the timer if it exists. Options 
 * exist to call a function when the timer expires, when the timer already 
 * exists, and a boolean to determine if the timer is rescheduled or not.
 */
class TimerMgr {

  /**
   * Constructor, creates a logger and timers dict.
   */
  constructor() {
    this.logger = log('rules_tools.TimerMgr');
    // Stores the timer and the functions:
    // - timer: timer Object
    // - notFlapping: function to call when timer expires
    // - flapping: function to call when check is called and timer already exists
    this.timers = {};
  }

  /**
   * Called when the timer expires. Cleans up the timer from the manager
   * and calls the passed in timer function when check was called.
   */
  _notFlapping(key) {
    this.logger.debug("Creating expire function");
    return function(context) {
      context.logger.debug('Timer expired for {}', key);
      if(key in context.timers && 'notFlapping' in context.timers[key]){
        context.logger.debug('Calling expired function {}', context.timers[key]['notFlapping']);
        context.timers[key]['notFlapping']();
      }
      if(key in context.timers) {
        context.logger.debug('Deleting the epired timer');
        delete context.timers[key];
      }
    }
  }

  /**
   * Function to call when null was passed for the func or flappingFunc.
   */
  _noop() {
    // do nothing
  }

  /**
   * If there is no timer associated with key, create one to expire at when and
   * call func (or _noop if func is null).
   * If there is a timer already associted with key, if reschedule is not 
   * supplied or it's false cancel the timer. If reschedule is true, reschedule
   * the timer using when. 
   * If there is a timer already associated with key, if a flappingFunc is 
   * provided, call it.
   * @param {*} key usually a String, the "name" of the timer
   * @param {*} when any representation of time of duration, see timeUtils.toDateTime
   * @param {function} func optional function to call when the timer expires
   * @param {boolean} reschedule optional flag, when present and true rescheudle the timer if it already exists
   * @param {function} flappingFunc optional function to call when the timer already exists
   */
  check(key, when, func, reschedule, flappingFunc) {
    this.logger.debug('Timer manager check called for {}', key);

    var timeout = timeUtils.toDateTime(when);
    this.logger.debug('Timer to be set for ' + timeout.toString());

    // timer exists
    if(key in this.timers){
      if(reschedule) {
        this.logger.debug("Rescheduling timer {} for {}", key, timeout.toString());
        this.timers[key]['timer'].reschedule(timeout); 
      }
      else  {
        this.logger.debug('Cancelling timer {}', key);
        this.cancel(key);
      }
      if(flappingFunc) {
        this.logger.debug('Running flapping function for {}', key);
        flappingFunc();
      }
    }

    // timer doesn't already exist, create a new one
    else {
      this.logger.debug('Creating timer for {}', key);
      var timer = actions.ScriptExecution.createTimerWithArgument(timeout, 
                                                                  this,
                                                                  this._notFlapping(key));
      this.timers[key] = { 'timer': timer,
                           'flapping': flappingFunc,
                           'notFlapping': (func) ? func : this._noop };
      this.logger.debug('Timer created for {}', key);
    }
  }

  /**
   * @param {*} key name of the timer
   * @returns {boolean} true if there is a timer assocaited with key
   */
  hasTimer(key) {
    return key in this.timers;
  }

  /**
   * If there is a timer assocaited with key, cancel it.
   * @param {*} key name of the timer
   */
  cancel(key) {
    if(key in this.timers) {
      this.logger.debug('Cancelling {}', key);
      this.timers[key]['timer'].cancel();
      delete this.timers[key];
    }
    else {
      this.logger.debug('No timer scheduled for {}', key);
    }
  }

  /**
   * Cancels all existing timers. Any timer that is actively running or 
   * has just terminated will be skipped and cleaned up in the _notFlapping
   * method.
   */
  cancelAll() {
    for(var key in this.timers) {
      var t = this.timers[key]['timer'];
      if(!t.hasTerminated() && !t.isRunning()) {
        this.logger.debug('Timer has not terminated, cancelling timer {}', key);
        this.cancel(key);
      }
      delete this.timers[key];
      this.logger.debug('Timer entry has been deleted for {}', key);
    }
  }
}

module.exports = {
  TimerMgr
}

loopingTimer

Sometimes you need a timer that repeats until some condition is met.

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


/**
 * @namespace LoopingTimer
 */

/**
 * Implements a looping Timer which is passed a function that is expected to return
 * a when supported by timeUtils.toDateTime. The loop will reschedule the timer based
 * on that returned when or, if it return null the looping stops.
 */
class LoopingTimer {

  /**
   * Constructor, creates a logger.
   */
  constructor() {
    this.logger = log('rules_toools.LoopingTimer');
    this.logger.debug("Looping Timer created");
  }

  /**
   * Kicks off the timer loop. Schedules a timer to call func at when
   * @param {function} func function to call at when, must return a when to continue the loop or null to stop
   * @param {*} when any of the types supported by timeUtils.toDateTime
   */
  loop(func, when) {
    this.logger.debug('Looping timer - loop called with {}', when);

    this.func = func;
    if(!when) this.expired();
    else {
      this.logger.debug('Creating new timer from loop');
      this.timer = actions.ScriptExecution.createTimer(
                                             timeUtils.toDateTime(when), 
                                             () => this.expired());
    }
  }

  /**
   * Called when the timer expires. Calls the passed in function and
   * reschedules it based on the returned when value, or ends if null was
   * returned.
   */
  expired() {
    this.logger.debug('expired called');
    var when = this.func();
    if(when) {
      this.logger.debug('Creating new timer in expired')
      this.timer = actions.ScriptExecution.createTimer(
                                             timeUtils.toDateTime(when),
                                             () => this.expired());
    }
  }

  /**
   * Cancels the timer if it exists and hasn't already terminated.
   */
  cancel() {
    if(this.timer && !this.hasTerminated()) {
      this.timer.cancel();
    }
  }

  /**
   * Returns true of the timer doesn't exist or has terminated.
   */
  hasTerminated() {
    return !this.timer || this.timer.hasTerminated();
  }
}

module.exports = {
  LoopingTimer
}

rateLimit

Sometimes no matter how often something occurs, you don’t want to take an action more often than a set period, ignoring the other events.

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

/**
 * Class that implements rate limiting. When the user calls run
 * the passed in function will only be called if enough time has 
 * passed since the last time run was called. The amount of time is defined
 * by the passed in when.
 */
class RateLimit {

  /**
   * Creates the logger and initializes the delay to be in the past so the
   * first time run is called it doesn't ignore the call.
   */
  constructor() {
    this.logger = log('rules_tools.RateLimit');
    this.until = time.ZonedDateTime.now().minusSeconds(1);
    this.logger.debug('ready to operate');
  }


  /**
   * User passes in a function to call and a delay. Subsequent calls to run will
   * be ignored until when from the last call to run has passed.
   * @param {function} func The function to call if enough time has passed
   * @param {*} when How long to wait before calling func again when run is called, can be anything supported by timeUtils.toDateTime.
   */
  run(func, when) {
    this.logger.debug('run called');
    const now = time.ZonedDateTime.now();
    if(now.isAfter(this.until)) {
      this.logger.debug("It's been enough time since the last time run was called, executing func");
      this.until = timeUtils.toDateTime(when);
      func();
    }
    else {
      this.logger.debug('Too soon, ignoring');
    }
  }
}

module.exports = {
  RateLimit
}

Rule Templates

All of my rule templates, which are how I implement a good percentage of my rules, are published to the Marketplace. I will not reproduce them here. Besides, those are essentially Nashorn rules that will run on JS Scripting anyway. I believe when you install them right now they will be running in Nashorn. They all need to be rewritten.

Rules

In no particular order:

Alarm Script

This rule gets called from my Alarm Clock Rule Template. When my alarm goes off in the middle of the night, turn on some lights briefly (I’ve a puppy that can’t make it through the night yet). When it goes off after 05:00, transition time of day to MORNING.

configuration: {}
triggers: []
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var logger = log('Alarm');


        var hour = time.ZonedDateTime.now().hour();

        var currToD = items.getItem('TimeOfDay').state;


        logger.info('Current ToD = {} Current Hour = {}', currToD, hour);

        if(currToD == 'NIGHT' || hour < 5) {
          logger.info("The alarm went off and it's night time, turning on the lights for ten minutes");
          items.getItem('TOD_Lights_ON_MORNING').sendCommand('ON');
          actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plusMinutes(5), function() {
            items.getItem('TOD_Lights_ON_MORNING').sendCommand('OFF');
          });
        }

        else if(currToD == 'BED'){
          logger.info('Good morning!');
          items.getItem('TimeOfDay').sendCommand('MORNING');
        }

        else {
          logger.warn("The alarm went off but it's not BED or NIGHT")
        }
    type: script.ScriptAction

All Away

I originally wrote this to debug some presence detection problem I was having and I’ve kept it in place and kept it up to date over time.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Presence
    type: core.ItemStateChangeTrigger
  - id: "3"
    configuration:
      itemName: vRichPresence
    type: core.ItemStateChangeTrigger
  - id: "4"
    configuration:
      itemName: vJennPresence
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "5"
    label: Triggering Item didn't change to uninitialized
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: "!items.getItem(event.itemName).isUninitialized"
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "2"
    label: Log out who left or returned home
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |-
        var {alerting} = require('personal');
        var logger = log('All Away');

        var item = items.getItem(event.itemName);
        var state = item.state;
        var name = item.getMetadataValue('name') || item.label.split("'")[0];

        logger.debug('{} has changed state', name);
        if(name == 'Everyone' && state == 'ON') {
          name = 'Someone';
        }

        var status = ' is unknown: ' + state;
        if(state == 'ON') {
          status = ' has arrived';
        }
        else if(state == 'OFF') {
          status = ' is away';
        }

        logger.info(name + status);

Calculate Electricity Bill

Estimate the month’s power bill based on the whole house energy meter.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: HomeEnergyMeter_ElectricmeterkWh
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: HomeEnergyMeter_Access
    type: core.ItemStateChangeTrigger
  - id: "3"
    configuration:
      itemName: HomeEnergyMeter_Rate
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "4"
    label: Calculate the power bill and update the Item
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var logger = log('Power Bill Estimate');


        var curr = items.getItem('HomeEnergyMeter_ElectricmeterkWh').rawState;

        var rate = items.getItem('HomeEnergyMeter_Rate').rawState;

        var access = items.getItem('HomeEnergyMeter_Access').rawState;

        var estimate = (curr * rate) + access;

        items.getItem('HomeEnergyMeter_CurrentBillEstimate').postUpdate(estimate.toString());

        logger.debug('Calculated bill = ${}', estimate);
    type: script.ScriptAction

Christmas Lights

This rule becomes disabled at the end of the season. Turn on the Christmas lights a little before sundown (based on Time of Day). On Christmas Eve and Christmas Day turn them on first thing in the morning.

The condition is probably redundant given I’ve a rule that disables this rule when TisTheSeason becomes OFF anyway.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: TimeOfDay
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: TisTheSeason
      state: ON
      operator: =
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        var logger = log('Christmas Lights');

        logger.info('Setting the christmas lights!');

        var newState = 'OFF';

        var currToD = items.getItem('TimeOfDay').state;
        var tods = 'AFTERNOON,EVENING';

        var daysto = actions.Ephemeris.getDaysUntil('Christmas');
        if(daysto <= 1) tods = tods + ',DAY';

        if(tods.contains(currToD)){
          newState = 'ON';
        }

        logger.info('Current tod = {} on tods = {}', currToD, tods);
        for each (var light in items.getItem('AllChristmasLights').members) {
          light.sendCommandIfDifferent(newState);
        }
    type: script.ScriptAction

Chromecast Alert

My eight-year-old like to sneak and mess around with the Chromecasts. This lets me know what is playing on what device.

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: Chromecast_Idle
      state: OFF
      previousState: ON
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var {alerting} = require('personal');

        var {timerMgr} = require('openhab_rules_tools');

        var logger = log('Rules CC Alert');


        timers = cache.get(ruleUID+'_tm', () => new timerMgr.TimerMgr());


        var alertFunctionGenerator = function() {
          return function() {
            // generate and send the alert if it's still on
            var idleItem = items.getItem('Chromecast_Idle');
            if(idleItem.state == 'ON') {
              var activeDevices = idleItem
                                    .members
                                    .filter(cc => cc.state == 'OFF')
                                    .map(function(cc) {
                                      var devName = cc.name.split(/_/)[0];
                                      return '  ' + cc.label.replace(' Idling', '') + ' | ' 
                                        + items.getItem(devName+'_App').state + ' | ' 
                                        + items.getItem(devName+'_MediaType').state + ' | ' 
                                        + items.getItem(devName+'_MediaArtist').state + ' | ' 
                                        + items.getItem(devName+'_MediaTitle').state;
                                    })
                                    .join('\n');
              alerting.sendInfo('The following Chromecast devices are now in use:\n' + activeDevices);
            }
            else {
              logger.debug('The Chromecast is no longer in use.');
            }
          }
        }


        logger.info('A Chromecast is now in use, waiting a bit for artist and track title info before sending the alert');

        timers.check('cc_alert', '60s', alertFunctionGenerator(), true);
    type: script.ScriptAction

Dad Motion

I’ve an instance of OH at my dad’s house with a motion sensor. If there is a period of time where he doesn’t move, I get an alert and can call and check in on him. I’m probably going to try to beef up the Threshold Alert template to handle this rule.

configuration: {}
triggers:
  - id: "2"
    configuration:
      itemName: Dads_Motion_Timeout
    type: core.ItemStateChangeTrigger
  - id: "4"
    configuration:
      startlevel: 100
    type: core.SystemStartlevelTrigger
  - id: "1"
    configuration:
      itemName: MotionSensor_LastMotion
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var {alerting} = require('personal');

        var {timeUtils, timerMgr} = require('openhab_rules_tools');


        var logger = log('Dad Motion');


        var formatDT = function(dt) {
          // TODO revist after this gets fixed in the library
          //var DateTimeFormatter = Java.type("java.time.format.DateTimeFormatter");
          //var formatter = time.DateTimeFormatter.ofPattern("h:mm a, ccc MMM dd");
          //return dt.format(formatter);
          return dt.toString();
        }


        var dadTimerExpiredGenerator = function(formatDT){
          return function() {
            if(items.getItem('Dads_Motion_Alert').state == 'ON') {
              alerting.sendInfo('It has been awhile since dad has moved. Last movement detected at ' 
                                + formatDT(items.getItem('MotionSensor_LastMotion').rawState.getZonedDateTime()));
            }
            else {
              logger.info("Dad's motion timer went off but alerting is OFF");
            }
          }
        }


        // Initialize variables

        var timers = cache.get(ruleUID+'_tm', () => new timerMgr.TimerMgr());

        var now = time.ZonedDateTime.now();

        var lastMotionItem = items.getItem('MotionSensor_LastMotion');

        var timeout = items.getItem('Dads_Motion_Timeout');


        var motionTime = (lastMotionItem.isUninitialized) ? now : timeUtils.toDateTime(lastMotionItem.rawState.getZonedDateTime());

        var motionTime_Str = formatDT(motionTime);

        var timerHours = 12;


        // If it's not set move the timout to 12 hours

        logger.debug('Timeout is {}', timeout.state);

        if(timeout.isUninitialized) {
          timeout.postUpdate('12');
        }

        else {
          timerHours = timeout.state;
        }


        // Calculate the alert time, move it to tomorrow if it'll go off at night

        var timerTime = motionTime.plusHours(timerHours);

        var nightStart = time.ZonedDateTime.now().withHour(22).withMinute(0).withSecond(0);

        var nightEnd = time.ZonedDateTime.now().withHour(9).withMinute(0).withSecond(0).plusDays(1);

        if(timerTime.isAfter(nightStart) && timerTime.isBefore(nightEnd)) {
          timerTime = nightEnd;
        }


        var timerTime_Str = formatDT(timerTime);


        // If it's already been too much time, alert now

        if(timerTime.isBefore(now)) {
          logger.warn('timerTime is in the past! {}', timerTime_Str);
          dadTimerExpiredGenerator(formatDT)();
        }

        // Set or reschedule a timer to alert if there is no motion for too much time

        else {
          logger.debug("Motion detected at Dad's at {} or reminder time has changed, setting reminder to expire at {}",
                      motionTime_Str, timerTime_Str);
          timers.check('alertTimer', timerTime, dadTimerExpiredGenerator(formatDT), true);
        }
    type: script.ScriptAction

Garage Alert

It’s really annoying to try and trigger the garage door openers when the controller is offline. This gives some feedback when that occurs.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Large_Garagedoor_Opener
    type: core.ItemCommandTrigger
  - id: "2"
    configuration:
      itemName: Small_Garagedoor_Opener
    type: core.ItemCommandTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: items.getItem('vCerberos_Status').state !=  'ON' ||
        items.getItem('Cerberossensorreporter_Onlinestatus').state != 'ON';
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var {alerting} = require('personal');

        alerting.sendAlert('Attempting to trigger a garage door but cerberos is not online!');
    type: script.ScriptAction

Humidifier Control

I’ve some dumb humidifiers connected to smart outlets. This controls them based on humidity readings and setpoints.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Mainfloorsensors_Humidity
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: Mainfloorsensors_Humidity_Setpoint
    type: core.ItemStateChangeTrigger
  - id: "3"
    configuration:
      itemName: MasterBedroomSensors_Humidity
    type: core.ItemStateChangeTrigger
  - id: "4"
    configuration:
      itemName: MasterBedroomSensors_Humidity_Setpoint
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "7"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var {utils} = require('personal');

        var logger = log('Humidifier Control');


        var HYST = 10;


        var sensorName = event.itemName.replace('_Setpoint', '');

        var setpointName = sensorName + '_Setpoint';

        var controlItem = items.getItem(sensorName + '_Switch');


        var curr = (event.itemName == sensorName)? event.itemState : items.getItem(sensorName).state;

        var max = (event.itemName == setpointName) ? event.itemState : items.getItem(setpointName).state;

        var min = max - HYST;


        logger.debug(sensorName + ' is currently ' + curr + ' with min ' + min + ' and max ' + max);


        var newState = utils.hysteresis(curr, max, HYST);


        logger.debug('New humidifier state is ' + newState + ' and Item state is ' + controlItem.state);


        if(newState != 'STAY' && controlItem.state != newState) {
          logger.debug('Command the humidifier ' 
                       + sensorName + ' to ' + newState 
                       + ' because min='+ min + ' max='+ max 
                       + ' and curr=' + curr);
          controlItem.sendCommand(newState);
        }

        else {
          logger.debug('Leaving the humidifier ' + controlItem.state);
        }
    type: script.ScriptAction

Humidity Alert

When the humidity drops too low it’s probably because a humidifier needs to be filled. This generates an alert. This is called by the Threshold Alert rule template.

configuration: {}
triggers: []
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |-
        var {alerting} = require('personal');
        var logger = log('Humidity Alert');
        var msg = 'Remember to fill the humidifiers. Minimum humidity is ' 
                  + items.getItem('MinIndoorHumidity').state 
                  + '. Low readings: ' 
                  + this.threshItemLabels;
        alerting.sendAlert(msg, logger);
    type: script.ScriptAction

Low Battery Alert

Let me know when a battery gets too low. This also is called by the Threshold Alert template.

configuration: {}
triggers: []
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var {alerting} = require('personal');

        var logger = log('Low Battery');

        alerting.sendAlert('The following batteries are below 25%: ' + this.threshItemLabels, logger);
    type: script.ScriptAction

MBR TV Stop at Night

My ISP has a bandwidth cap. :angry: My wife likes to fall to sleep to the sound of the TV. So this rule will stop the TV well after we’ve all fallen to sleep.

configuration: {}
triggers:
  - id: "2"
    configuration:
      time: 01:00
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    label: Stop playback on MBR Chromecast
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        var logger = log('TV Home');
        logger.info('Stopping the Master Bedroom TV');
        items.getItem('MasterBedroomTV_Stop').sendCommand('ON');
    type: script.ScriptAction

MBR Lights ON when Returning

If it’s dark turn on the master bedroom lights when we get home.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Presence
      state: ON
      previousState: OFF
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: MasterBedroomSensors_LightLevel
      state: "100"
      operator: <
    type: core.ItemStateCondition
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        var {alerting} = require('personal');
        !alerting.isNight()
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        var logger = log('MBR Lights');

        logger.info('Welcome home! Turning on the MBR Lights!');
        items.getItem('MasterBedroomLights_Power').sendCommand('ON');
    type: script.ScriptAction

Open Door Night Time

If it’s nearing bed time or no one is home, alert if a door is OPEN. Repeat the warning until the doors are closed.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: TimeOfDay
      state: NIGHT
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: Presence
      state: OFF
      previousState: ON
    type: core.ItemStateChangeTrigger
  - id: "4"
    configuration:
      itemName: DoorsStatus
      state: OPEN
    type: core.ItemStateChangeTrigger
  - id: "6"
    configuration:
      itemName: TimeOfDay
      state: BED
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "5"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |-
        var {alerting} = require('personal');
        var logger = log('Open Door Night')
        logger.debug('State = {} not closed = {} away = {}',
                     event.itemState,
                     (event.itemState != 'CLOSED'),
                     alerting.isAway());
        event.itemState != 'CLOSED' && alerting.isAway();
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var {alerting} = require('personal');

        var {loopingTimer} = require('openhab_rules_tools');

        var logger = log('Open Door Night Reminder');


        var lt = cache.get(ruleUID+'_lt', () => new loopingTimer.LoopingTimer());


        var reminder = function() {

          var doorsStatus = items.getItem('DoorsStatus');
          var state = doorsStatus.state;
          logger.info("It's night or all are away, sending alerts until all doors are closed, doors status " 
                       + state);
            
          if(state == 'OPEN') {
            logger.info('Sending open door alert');
              
            var openDoors = alerting.getNames(doorsStatus, door => door.state == 'OPEN');
            var tod = items.getItem('TimeOfDay').state;
            var pres = items.getItem('Presence').state;
              
            var msg = 'The following doors are open';
            msg += (alerting.isNight()) ? " and it's night time" : '';
            msg += (pres == 'OFF') ? ' and no one is home: ' : ': ';
            msg += openDoors;
                
            alerting.sendAlert(msg);
            return '15m';
          }
          else {
            logger.info('All doors are now closed');
            return null;
          }  
        }


        // Kick off the looping timer

        lt.loop(reminder, '0s');
    type: script.ScriptAction

Open Door Reminder

Each door sensor Item has some metadata with a time. If the door remains open for longer than that time send an alert.

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: DoorsStatus
      state: OPEN
    type: core.GroupStateChangeTrigger
  - id: "4"
    configuration:
      groupName: DoorsStatus
      state: CLOSED
    type: core.GroupStateChangeTrigger
conditions:
  - inputs: {}
    id: "2"
    label: The door's state didn't change to an UnDefType
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        !items.getItem(event.itemName).isUninitialized;
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var {alerting} = require('personal');

        var {timerMgr} = require('openhab_rules_tools');

        var logger = log('Open Door Reminder');


        var timers = cache.get(ruleUID+'_tm', () => new timerMgr.TimerMgr());


        var reminderGenerator = function(itemName, name, when, timers){
          return function() {
            var item = items.getItem(itemName)
            if(item.state != 'OPEN') {
              logger.warn(itemName + ' open timer expired but the door is ' 
                          + itemState + '. Timer should have been cancelled.');
            }
            else {
              alerting.sendAlert(name + ' has been open for ' + when);
              // Reschedule if it's night
              var tod = items.getItem('TimeOfDay').state;
              if(alerting.isNight()) {
                logger.info('Rescheduling timer for ' + name + " because it's night");
                timers.check(itemName, when, reminderGenerator(itemName, name, when));
              }
            }
          }
        }


        var item = items.getItem(event.itemName);

        logger.debug('Handling new door state for reminder: ' + item.name + ' = ' + item.state);

        if(item.state == 'CLOSED' && this.timers.hasTimer(item.name)) {
          logger.info('Cancelling the timer for ' + item.name);
          this.timers.cancel(item.name);
        }

        else {
          var name = item.getMetadataValue('name') || item.label.replace(' Sensor', '').replace(' Status', '');
          var remTime = item.getMetadataValue('rem_time') || '60m';
          logger.debug('Creating a reminder timer for ' + item.name + ' for ' + remTime);
          timers.check(item.name, remTime, reminderGenerator(event.itemName, name, remTime, timers));
        }
    type: script.ScriptAction

SSID Presence

My presence detection has evolved over time. The Android app’s ability to report the connected SSID is the simplest and more reliable I’ve implemented so far. This looks to see if it’s connected to my home SSID.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: RichSSID
      state: ""
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: JennSSID
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var logger = log('SSID Presence');

        logger.info('{} changed to {}', event.itemName, event.itemState);


        var ssids = 'myssid';

        var newState = (ssids.contains(event.itemState.toString())) ? 'ON' : 'OFF';


        var name = event.itemName.replace('SSID', '');

        logger.info('{} presence is {}', name, newState);


        var item = items.getItem('v'+name+'Phone');

        if(item.state != newState) {
          logger.info('Commanding {} to {}', item.name, newState);
          item.sendCommand(newState);
        }

        else {
          logger.info('{} is already {}', item.name, newState);
        }
    type: script.ScriptAction

Reset Powerbill

At the end of the billing month record the estimated power bill and start calculating the next month’s.

configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 0 0 9 * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var logger = log('Reset Power Estimate');


        logger.info("It's the first day of the electricity billing period, time to reset the running total.");


        items.getItem('HomeEnergyMeter_LastMonthBillEstimate').postUpdate(items.getItem('HomeEnergyMeter_CurrentBillEstimate').state);

        items.getItem('HomeEnergyMeter_ResetMeter').sendCommand('ON');

        items.getItem('HomeEnergyMeter_CurrentBillEstimate').postUpdate(items.getItem('HomeEnergyMeter_Access').state);
    type: script.ScriptAction
9 Likes

Service Offline Alerting

Send an alert when a home automation important service goes offline.

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: ServiceStatuses
      state: ON
    type: core.GroupStateChangeTrigger
  - id: "2"
    configuration:
      groupName: ServiceStatuses
      state: OFF
    type: core.GroupStateChangeTrigger
conditions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |+
        var logger = log('Offline Alert');

        logger.debug("Offline alert for {} state {} old state {} undef {}", 
                     event.itemName, 
                     event.itemState, 
                     event.oldItemState, 
                     items.getItem(event.itemName).isUninitialized);
        !items.getItem(event.itemName).isUninitialized;

    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var {alerting} = require('personal');

        var {timerMgr} = require('openhab_rules_tools');

        var logger = log('Offline Alert');


        logger.debug('Service offline alert for {}  which is {}!', event.itemName, event.itemState);

        var timers = cache.get(ruleUID+'_tm', () => new timerMgr.TimerMgr());


        var alertGenerator = function(itemName, itemState) {
          return function() {
            var curr = items.getItem(itemName);
            if(curr.state == itemState) {
              var status = (itemState == 'ON') ? 'online' : 'offline';
              var name = item.getMetadataValue('name') || item.label;
              alerting.sendAlert(name + ' is now ' + status + '!', logger);
            }
            else {
              logger.warn('Offline alert timer expired but Item ' + itemName 
                          + " has returned to it's previous state!")
            }
          }
        }


        timers.check(event.itemName, '5m', 
                     alertGenerator(event.itemName, event.itemState.toString()));
    type: script.ScriptAction

Service Offline Report

Send an alert with a summary of offline devices in the morning, if there are any.

configuration: {}
triggers:
  - id: "1"
    configuration:
      time: 08:00
    type: timer.TimeOfDayTrigger
conditions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: ServiceStatuses
      state: ON
      operator: "!="
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var {alerting} = require('personal');

        var logger = log('Offline Report');


        var ss = items.getItem('ServiceStatuses');

        var nullItems = alerting.getNames('ServiceStatuses', s => s.isUninitialized);

        if(nullItems) logger.info('Null: {}', nullItems);

        else logger.debug('No null services');


        var offItems = alerting.getNames('ServiceStatuses', s => s.state == 'OFF');

        if(offItems) logger.info('OFF Items: {}', offItems);

        else logger.debug('No OFF services');


        var msg = '';


        if(nullItems) {
          msg = 'The following sensors are in an unknown state: ' + nullItems;
        }

        if(offItems) {
          if(msg) msg += '\n';
          msg += 'The following sensors are known to be offline: ' + offItems;
        }


        if(msg) alerting.sendInfo(msg);

        else logger.info('There are no offline services');
    type: script.ScriptAction

Thing Status Handler

I don’t know why I still have this around. I wrote it to test the Thing Status rule template, which calls this script. Thing Status Reporting

configuration: {}
triggers: []
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var logger = log('Thing Status Processor');


        logger.debug("There are " + this.things.length + " things that are not ONLINE");
    type: script.ScriptAction

Time of Day Lights

The actual Time of Day rule is a Rule Template: Time Based State Machine.

Based on the time of day transitions, turn OFF and ON the lights for that period.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: TimeOfDay
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var logger = log('Time of Day Lights');


        var tod = items.getItem('TimeOfDay').state

        var offGroupName = 'TOD_Lights_OFF_' + tod

        var onGroupName = 'TOD_Lights_ON_' + tod


        var switchLights = function(tod, grp, st) {
          logger.info('Turning ' + st + ' the lights for ' + tod + ' using ' + grp);
          items.getItem(grp)
            .members
            .filter(light => light.state != st)
            .forEach(light => light.sendCommandIfDifferent(st));
        }


        switchLights(tod, offGroupName, "OFF");

        switchLights(tod, onGroupName, "ON");
    type: script.ScriptAction

Water Leak Alarm

After having to fix the ceiling in the basement under the kitchen sink, I got a couple of Zigbee water leak alarms. This rule is mainly to just back up the “beep beep beep” from the alarms.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: WaterLeakAlarms
      state: ON
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var {alerting} = require('personal');

        var {loopingTimer} = require('openhab_rules_tools');

        var logger = log('Water Leak Alert');

        logger.warn('A water leak was detected!');


        var lt = cache.get(ruleUID+'_lt', () => new loopingTimer.LoopingTimer());


        var loopGenerator = function() {
          return function(){
            if(items.getItem('WaterLeakAlarms').state == 'OFF') {
              logger.info('No more leaks!');
              return null;
            }
            else {
              logger.info('Still seeing a water leak!');
              var names = alerting.getNames('WaterLeakAlarms', i => i.state == 'ON');
              alerting.sendAlert("There's a leak at " + names + '!');
              return '1m';
            }
          }
        }
          
        lt.loop(loopGenerator(), '0s');
    type: script.ScriptAction

Weather Lights

During the “DAY” time, turn on or off the lights based on cloudiness level. However, if a light is overridden (based on metadata) leave it alone.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: vIsCloudy
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: TimeOfDay
      state: DAY
    type: core.ItemStateChangeTrigger
  - id: "5"
    configuration:
      itemName: Presence
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: TimeOfDay
      state: DAY
      operator: =
    type: core.ItemStateCondition
  - inputs: {}
    id: "6"
    label: vIsCloudy isn't undefined
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        !items.getItem('vIsCloudy').isUninitialized;
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var logger = log('Weather Lights');


        if(items.getItem('Presence').state == 'OFF') {
          logger.info('No one is home, turing off the weather lights');
          items.getItem('TOD_Lights_ON_WEATHER').sendCommand('OFF');
        }


        else {
          // TODO find alternative to sleep
        //  if(event !== undefined && event.itemName == "TimeOfDay") {

        //    java.lang.Thread.sleep(500);

        //  }
          
          logger.info('Cloudiness changed to ' + items.getItem('vIsCloudy').state + ', adjusting the lights');
          items.getItem('TOD_Lights_ON_WEATHER')
               .members
               .filter(light => light.getMetadataValue('LightsOverride') == 'false' || !light.getMetadataValue('LightsOverride'))
               .forEach(light => light.sendCommandIfDifferent(items.getItem('vIsCloudy').state));
        }
    type: script.ScriptAction

Weather Lights Override Reset

Sets the override metadata to false at the end of the “DAY” time of day.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: TimeOfDay
      previousState: DAY
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: "var logger = log('Reset Lights Override');


        logger.info('Resetting the LightsOverride metadata value to
        false');


        logger.debug('Looping through the lights');

        for(let light of items.getItem('TOD_Lights_ON_WEATHER').members)
        {

        \  logger.debug('Current override is {}',
        light.getMetadataValue('LightsOverride'));

        \  logger.debug('Success = {}',
        light.upsertMetadataValue('LightsOverride', 'false'));

        }

        \    "
    type: script.ScriptAction

Not sure why the formatting is weird for this one.

Weather Lights Override

If a manual change to a light is detected mark that light as overridden. This is somewhat brittle becuase it’s based on timing.

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: TOD_Lights_ON_WEATHER
    type: core.GroupStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: TimeOfDay
      state: DAY
      operator: =
    type: core.ItemStateCondition
  - inputs: {}
    id: "4"
    label: Manual trigger?
    description: TimeOfDay changed more than 10 seconds ago and vIsCloudy more than
      5 seconds ago
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var {timeUtils} = require('openhab_rules_tools');

        var logger = log('Weather Lights Override');

        logger.debug('Checking to see if enough time has past since a transition');


        var isBefore = function(item, secs) {
          return timeUtils.toDateTime(item.history.lastUpdate('mapdb')).isBefore(time.ZonedDateTime.now().minusSeconds(secs));
        }


        var tod = isBefore(items.getItem('TimeOfDay'), 10);

        var cloudy = isBefore(items.getItem('vIsCloudy'), 5);


        tod && cloudy;
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var logger = log('Weather Lights Override');


        logger.info('Manual light trigger detected, overriding the light for {}', event.itemName);

        items.getItem(event.itemName).upsertMetadataValue("LightsOverride", "true");
    type: script.ScriptAction

Zwave Controller Offline Alert

For a time I was having my HUSZB-1 controller go offline periodically. This rule was to let me know when that happens. I think it was something caused by Docker. I’ve just never removed the rule.

configuration: {}
triggers:
  - id: "1"
    configuration:
      thingUID: zwave:serial_zstick:zw_controller
      status: OFFLINE
    type: core.ThingStatusChangeTrigger
  - id: "2"
    configuration:
      thingUID: zigbee:coordinator_ember:zg_coordinator
      status: OFFLINE
    type: core.ThingStatusChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        var {alerting} = require('personal');

        var {timerMgr, rateLimit} = require('openhab_rules_tools');


        var timers = cache.get(ruleUID+'_tm', () => new timerMgr.TimerMgr());

        var rl = cache.get(ruleUID+'_rl', () => new rateLimit.RateLimit());


        var sendAlertGen = function() {
          return function() { rl.run(() => alerting.sendAlert('The Zwave Controller or Zigbee Coordinator is OFFLINE!'), '24h'); }  
        };


        timers.check('zigbee', '1m', sendAlertGen(), true);
    type: script.ScriptAction

Stuff to look for

  • Notice how much simpler rules can become when consolidating common code into a library, even if it’s just a couple of lines function. The above would be hundreds if not a thousand lines longer if I had to rewrite the TimerMgr code for every rule that uses it. Just look at the Rule Templates which have to include these library classes inline to see what I mean.

  • These are just my JS Scripting rules. I’ve also a bunch of just pure UI rules without code as well as roughly 25% of my rules implemented by rule templates. You are not stuck with just one language.

  • All rules share the cache, take special care with the keys used to put stuff into the cache. I use the ruleUID as part of the key.

  • The cache remains even if you save and reload the rule. Don’t forget to delete the old variable (e.g. TimerMgr), especially if you’re changing the code for what’s stored there. While writing the looping timer I couldn’t figure out why my changes to the code were not being reflected in the rule. It was because an instance of LoopingTimer was still in the cache and not being recreated with the new code.

  • Conditions can be pretty useful to simplify some rules.

  • Pay attention to the JavaScript Array filter, map, reduce type operations.

  • I pretty much only have to import my libraries. Everything else I need to work with OH is just there in openhab-js.

12 Likes

Excellent. Thank you very much.
Thread bookmarked and has become now my personal JS scripting bible :slight_smile:

@rlkoshak Great examples, I can‘t wait for a npm package with timeUtils and especially timerMgr, as those are my most used libs.

I would like to share my rules (but they are file-based!!), which are hosted at openhab-conf/automation/js at main · florian-h05/openhab-conf · GitHub.

Futhermore, I am working on a JS Scripting toolkit that is available at npm, that currently provides:

  • a scene engine to call pre-defined scenes
  • an alarm clock that can be fully configured with items

If you are interested, head over to Package openhab-tools · florian-h05/openhab-js-tools · GitHub.
The GitHub repo is GitHub - florian-h05/openhab-js-tools: Tools for the openHAB JavaScript Automation Add-On..
NOTE: Currently, I am thinking of moving the toolkit to the GitHub package registry, therefore it could happen that it is removed from npm!

I’ve made something like this as a rule template. See Creating Capabilities with Rule Templates: Time of Day.

My main goal is for most users to not have to know anything about npm and only rarely need to code anything themselves. And when they do they can do everything they need to with Blockly.

But to get there I we need more rule templates. a so I encourage any advanced users to please consider posting rule templates instead of our in addition to npm modules. Honestly, npm modules help the IH users who need the help the least.

That’s not too say there isn’t usefulness in modules, but a full rule in an npm module is going to bed it of reach of most the least gracias of our users, the users who are heat able to code something like that themselves.

I understand your point, I was thinking about implementing that alarm clock in a rule template (I think with alarm clock it is possible), but e.g. for the sceneEngine I do not know whether it is possible to implement it‘s function in a rule template.

Well, look at what has already been published as rule templates. I’ve already got an Alarm Clock rule template posted and @JustinG published scene control rule templates so I’m sure it would be possible. If it can be written as a rule, it can be converted to be a template.

I’m finding rule templates to be more powerful when they do one thing and one thing well, and then the rules call each other in a chain. That’s how most of my rules work (for example a full simple implementation of the time of day DP can be created with the Alarm Clock template, To Today template, and the DateTime Custom Widgets. (Note that the ephemeris time of day is implemented in a separate rule template).

Alternatively, rule templates also work great when they provide a turn-key capability like Justin’s scene controller, including one or more rules and custom UI widgets.

Openhab Configuration

##   Release = Debian GNU/Linux 10 (buster)
##    Kernel = Linux 4.19.0-16-amd64
##  Platform = VMware Virtual Platform/440BX Desktop Reference Platform
##    Uptime = 0 day(s). 0:1:27
## CPU Usage = 100% avg over 1 cpu(s) (1 core(s) x 1 socket(s))
##  CPU Load = 1m: 1.06, 5m: 0.24, 15m: 0.08
##    Memory = Free: 1.20GB (62%), Used: 0.74GB (38%), Total: 1.94GB
##      Swap = Free: 0.95GB (100%), Used: 0.00GB (0%), Total: 0.95GB
##      Root = Free: 6.79GB (48%), Used: 7.18GB (52%), Total: 14.74GB
##   Updates = 0 apt updates available.
##  Sessions = 1 session(s)
##  Release Build = 3.2.0

Objective
Within a js script I wish to issue a number of API calls–some with tokens and others with cookies–and map the json response to items. I’ve tried a number of HTTP related commands with no success when it comes to cookies and tokens. I felt I could do this with ECMAScript-2021 and loading the node-fetch module but I believe I’m going down a rat hole with no end in sight.

Challenge
I installed node-fetch using the following commands at /etc/openhab/automation/js directory

sudo -u openhab npm install node-fetch@2

I verified that node-fetch module is in the node-modules directory. I added const fetch = require('node-fetch'); on the first line of a simple js script that I verified it works.

const fetch = require('node-fetch'); //just added this line to verify I could call the library
var now = time.ZonedDateTime.now();
var yesterday = time.ZonedDateTime.now().minusHours(24);

var item = items.getItem("WDowneyOCA_ApparentTemperature");
console.log("averageSince", item.history.averageSince(yesterday));

When I run it, I get the following error

org.graalvm.polyglot.PolyglotException: TypeError: Cannot load CommonJS module: ‘stream’

I then installed stream module but it fails again saying that the emitter module is not present. I suspect I’m not referencing the library correctly. Can someone clarify what I’m doing wrong.

Thanks

It looks like openhab jsscripting does not include nodejs runtime(*). What does this mean? All js packages from npm (such as fetch) that rely on modules listed in Index | Node.js v17.3.1 Documentation will not work. For some of the nodejs runtime modules there exists versions meant for browsers (not relying on nodejs), such as the “stream” javascript npm package.

There a lot of packages that will work just fine but I can imagine all functionality related to OS APIs (file system, networking, OS threads, starting new processes etc) will not be available.

Do we have to fallback to the functionality of java in these cases?

Cc @rlkoshak

(*) graalvm javascript seems to include the nodejs runtime if started on a particular way. From GraalVM :

The Node.js runtime cannot be embedded into a JVM but has to be started as a separate process.

However, separate process would not work with the current single process design of openHAB. At least this is my understanding…

To my understanding openhab is not starting the separate process currently.

Yes, of course. All of Java is available to you with a Java.type('name.of.class') to import it.

However, beware of anything that messes with threading as GraalVM throws an exception when trying to mess with threads, such as Thread.sleep().

So I should not use the require statement to import the node-module? How would I import node-fetch module using Java.type command?

I don’t think that’s what @ssalonen is saying. What he is saying is that not all Node modulkes can be imported without having an instance of something (node I guess) running along side of OH.

You wouldn’t. You would use the Java HTTP classes instead of node-fetch as a fall back.

as @rlkoshak said… I do not think you can get node-fetch to work with openHAB since it eventually relies on http builtin module provided by nodejs runtime which just isn’t there with openHAB jsscripting environment.

If the http utilities offered by openhab-js is not enough (see actions - Documentation and JavaScript Scripting - Automation | openHAB), you need to revert to java libraries meant for HTTP. In openHAB, jetty is used (ref). You hopefully can just “import” the jetty functions using Java.type function – I haven’t tested this.

As you can see it gets quite complex unfortunately and you need both javascript and java skills to make it happen.

Adding a few more point examples. I have these as a Scripts for future reference.

Create a rule from a rule

The rule will disappear when OH restarts unless this code is run again. You cannot set the UID when using builder as far as I can tell. Deleting the rule does not appear to work.

// Delete doesn't work
const { ruleRegistry } = require('@runtime/RuleSupport');
let RuleManager = osgi.getService("org.openhab.core.automation.RuleManager");
console.info('Creating a rule');

// rule builder
var ruleBuilder = function() {
  rules.when().item('aTestSwitch').receivedCommand().then(event => {
    console.log(event);
  }).build("Test rule from rule", "Test rule from rule");
}

var ruleJSRule = function() {
  rules.JSRule({
    name: 'Test rule from rule2',
    description: 'Test rule from rule2',
    id: ruleUID+'_test_jsrule',
    triggers: [triggers.ItemCommandTrigger('aTestSwitch')],
    execute: event => {
      console.log(event);
    }
  });
}

//ruleBuilder();
ruleJSRule();
// See JS Scripting docs.

console.info("Triggering created rule");
items.getItem('aTestSwitch').sendCommand('ON');

console.info('Deleting the rule');

var allRules = utils.javaSetToJsArray(ruleRegistry.getAll());
var uids = allRules.filter(r => r.getName() == "Test rule from rule2");

if(uids.length > 0) {
  console.info('Found some rules', uids.length, uids[0].getUID());  
  const rval = ruleRegistry.remove(uids[0].getUID());
  if(!rval) console.error("failed to remove rule!", rval);
}

var status = RuleManager.getStatus(uids[0].getUID());

if(status) {
  console.info(status);
}
else {
  console.info("Rule no longer exists");
}

Disable/Enable another rule

const { ruleRegistry } = require('@runtime/RuleSupport');
let RuleManager = osgi.getService("org.openhab.core.automation.RuleManager");

var logger = log('examples');

// Get all the rules
var allRules = utils.javaSetToJsArray(ruleRegistry.getAll());
logger.info('There are {} rules', allRules.length);
for each (let r in allRules) {
  logger.info('Rule name = {} Rule UID = {}', r.getName(), r.getUID());
}

logger.info("This rule's UID = {}", ruleUID);

// Disable this rule
logger.info('Enabled status before: {}', RuleManager.isEnabled(ruleUID));
RuleManager.setEnabled(ruleUID, false);
logger.info('Enabled status after: {}', RuleManager.isEnabled(ruleUID));

Ephemeris

let logger = log('ephemeris example');
logger.info('About to test Ephemeris');
logger.info((actions.Ephemeris.isWeekend()) ? "It's the weekend" : 'Get back to work!');

executeCommandLine

var results = actions.Exec.executeCommandLine(time.Duration.ofSeconds(1), 'echo', 'hello');
console.info(results);

Working with QuantityTypes

var QT = Java.type('org.openhab.core.library.types.QuantityType');

let cloudiness = items.getItem('vCloudiness');
console.info('Cloudiness.rawState.class = ' + cloudiness.rawState.class.toString());
console.info('Cloudiness.state = ' + cloudiness.state); // string
console.info('Cloudiness.rawState = ' + cloudiness.rawState); // QuantityType
console.info('Cloudiness as number = ' + cloudiness.rawState.floatValue()); // float
console.info('Compare to 50%: ' + (cloudiness.rawState.compareTo(QT.valueOf('50 %')) < 0));

Run another rule

let RuleManager = osgi.getService("org.openhab.core.automation.RuleManager");

RuleManager.runNow('tocall');
var map = { 'test_data': 'Passed data to called rule' }
RuleManager.runNow('tocall', true, map);

In tocall the passed in test_data is available as test_data.

Here’s tocall

var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Run From");
logger.info("Map is: " + context.getAttribute("test_data"));

Complex coordination with timing

JS Scripting doesn’t support Thread::sleep, nor does it support having more than one thread running at the same time. This means you cannot have the script running at the same time as a timer nor can you have two timers running at the same time from the same script. Here is one way to work around that which I’ve used in my “unit tests” I’ve created for one of my libraries.

This is testing the Gatekeeper class.

Note it also shows examples for creating Timers.

var {gatekeeper, testUtils} = require('openhab_rules_tools');
var logger = log('rules_tools.Gatekeeper Tests');

var testFunGen = (test, num) => {
  logger.debug('generating function for {} run{}', test, num);
  return () => {
    logger.debug('{}: Test {} ran', test, num);
    cache.put(test+'_run'+num, time.ZonedDateTime.now());
  };
}

logger.info('Starting Gatekeeper tests');

var reset = (test) => {
  logger.debug('resetting {}');
  cache.put(test, null);
  logger.debug('resetting {}', test+'_start');
  cache.put(test+'_start', null);
  for(var i = 1; i <= 4; i++ ) {
    logger.debug('resetting {}', test+'_run'+1)
    cache.put(test+'_run'+i, null);
  }
}

// Test 1: Scheduling
var gk1 = new gatekeeper.Gatekeeper();
var TEST1 = ruleUID+'_test1';
reset(TEST1);
cache.put(TEST1+'_start', time.ZonedDateTime.now());
gk1.addCommand('1s', testFunGen(TEST1, 1));
gk1.addCommand('2s', testFunGen(TEST1, 2));
gk1.addCommand('3s', testFunGen(TEST1, 3));
gk1.addCommand(500, testFunGen(TEST1, 4));

actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plus(6500, time.ChronoUnit.MILLIS), () => {
  var success = true;
  const start = cache.get(TEST1+'_start');
  const run1 = cache.get(TEST1+'_run1');
  const run2 = cache.get(TEST1+'_run2');
  const run3 = cache.get(TEST1+'_run3');
  const run4 = cache.get(TEST1+'_run4');
  if(start === null) {
    logger.error('{} Failed to get starting timestamp', TEST1);
    success = false;
  }
  if(success && run1 === null) {
    logger.error('{} run1 failed to run!', TEST1);
    success = false;
  }
  if(success && run2 === null) {
    logger.error('{} run2 failed to run!', TEST1);
    success = false;
  }
  if(success && run3 === null) {
    logger.error('{} run3 failed to run!', TEST1);
    success = false;
  }
  if(success && run4 === null) {
    logger.error('{} run4 failed to run!', TEST1);
    success = false;
  }
  
  if(success) {
    logger.info('\n{}\n{}\n{}\n{}\n{}', start.toString(), run1.toString(), run2.toString(), run3.toString(), run4.toString());
    const dur1 = time.Duration.between(run1, run2).seconds();
    const dur2 = time.Duration.between(run2, run3).seconds();
    const dur3 = time.Duration.between(run3, run4).seconds();
  
    if(start.isAfter(run1)) {
      logger.error('{} failed, run1 ran before start!', TEST1);
      success = false;
    }
    if(success && dur1 != 1) {
      logger.error('{} failed, time between run1 and run2 is {} seconds.', dur1);
      success = false;
    }
    if(success && dur2 != 2) {
      logger.error('{} failed, time between run2 and run3 is {} seconds', dur2);
      success = false;
    }
    if(success && dur3 != 3) {
      logger.error('{} failed, time between run3 and run4 is {} seconds', dur3);
    }
    if(success) {
      logger.info('Gatekeeper test 1 success!');
    }
  }
});

// Test 2: cancelAll
var gk2 = new gatekeeper.Gatekeeper();
var TEST2 = ruleUID+'_test2'
reset(TEST2);
gk2.addCommand('1s 500z', testFunGen(TEST2, 1));
gk2.addCommand('2s', testFunGen(TEST2, 2));
gk2.addCommand('3s', testFunGen(TEST2, 3));
gk2.addCommand(500, testFunGen(TEST2, 4));

actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plus(2750, time.ChronoUnit.MILLIS), () => {
  var success = true;
  const run1 = cache.get(TEST2+'_run1');
  const run2 = cache.get(TEST2+'_run2');
  const run3 = cache.get(TEST2+'_run3');
  const run4 = cache.get(TEST2+'_run4');
  
  if(!run1) {
    logger.error('{} failed, run1 did not run', TEST2);
    success = false;
  }
  if(success && !run2) {
    logger.error('{} failed, run2 did not run', TEST2);
    success = false;
  }
  if(success && run3) {
    logger.error('{} failed, run3 ran too soon', TEST2);
    success = false;
  }
  if(success && run4) {
    logger.error('{} failed, run4 ran too soon', TEST2);
    success = false;
  }
  if(success) {
    gk2.cancelAll();
    actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plus(4000, time.ChronoUnit.MILLIS), () => {
      var success = true;
      const run3 = cache.get(TEST2+'_run3');
      const run4 = cache.get(TEST2+'_run4');
      
      if(run3) {
        logger.error('{} failed, run3 ran after being cancelled');
        success = false;
      }
      if(success && run4) {
        logger.error('{} failed, run4 ran after being cancelled');
      }
      if(success){
        logger.info('Gatekeeper test 2 success!')
      }
    });
  }
});
1 Like