Some JS Scripting UI Rules Examples

Edit1: see Some JS Scripting UI Rules Examples - #14 by rlkoshak for some more point examples.
Edit 2: Removed direct quoting of openhab_rules_tools, changed to installation instructions. The actual libraries are constantly being expanded and improved and I don’t want to keep them up to date here too.

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 completed an initial release of my openhab_rules_tools libraries ported to JS Scripting and as an npm module. See Announcing the initial release of openhab_rules_tools.

Basically all of my rules depend upon this library which includes a number of useful capabilities mostly having to deal with managing timers and controlling the scheduling of events.

For full docs, installation and usage examples see https://github.com/rkoshak/openhab-rules-tools.

tl;dr:

cd $OH_CONF/automation/js
sudo -u openhab npm i openhab_rules_tools

For now, use the comments in the code, the examples here, and the tests for usage. Eventually I’ll generate some reference docs.

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. They all need to be rewritten. These templates actually generate almost half of portion of my rules.

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
12 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.

14 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 v21.7.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 Redirecting… :

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!')
      }
    });
  }
});
3 Likes

Hi Rich,

From Dad motion or Service Offline Alerting I see you use: var timers = cache.get(ruleUID+'_tm', () => new timerMgr.TimerMgr());. Do the timers get stored in the cache because you use the TimerMgr as a class? I can’t see anywhere a cache.put() so I wondered how these timers end up in the cache in the first place…

Thanks

There is an implicit put when you call get with a default. So that line returns the value associated with ruleUID+'_tm', or if that’s null, create a new TimerMgr and put that as the value for that key.

That’s how it works in practice anyway.

Thanks. Got it :slight_smile:

Hi,
I was looking for a rule to have a notification when a window was forgotten opened, and I found your scripts and libraries. After one day I found how to install but I cannot understand why, into your rule, I cannot reschedule the timer.

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

`
and another strange thing, If I modify the rule code, I have a timer created when the window goes on CLOSED too.

If you’re going to use the library you need to be sure to read how it works which means, for now at least, reading the comments in the code. You will see that check takes up to five arguments. The last two control what happens when the timer already exists.

:person_shrugging: Modify how? How is the rule triggered?

I have modified the rule to don’t check the TimeOfDay because I still want to have a reminder forever until I close the window(s).
This is my rule:

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: gFinestre
      state: OPEN
    type: core.GroupStateChangeTrigger
  - id: "4"
    configuration:
      groupName: gFinestre
      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 ' 
                          + item.state + '. 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, timers), true);
                logger.info('Creating a new reminder timer for ' + itemName + ' for ' + when); //logger.debug
              //}
            }
          }
        }


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

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

        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') || '1m';
          logger.info('Creating a reminder timer for ' + item.name + ' for ' + remTime); //logger.debug
          timers.check(item.name, remTime, reminderGenerator(event.itemName, name, remTime, timers));
        }
    type: script.ScriptAction

But I cannot re-schedule the timer. I think I made something wrong but I didn’t understand where.

For the problem for CLOSED, this is what happens in my log:

2022-02-03 16:20:21.875 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item ‘GF_Bathroom_Finestra’ changed from UNDEF to CLOSED
2022-02-03 16:20:21.922 [INFO ] [automation.script.open door reminder] - Handling new door state for reminder: GF_Bathroom_Finestra = CLOSED
2022-02-03 16:20:21.930 [INFO ] [automation.script.open door reminder] - Creating a reminder timer for GF_Bathroom_Finestra for 1m
2022-02-03 16:21:21.956 [WARN ] [automation.script.open door reminder] - GF_Bathroom_Finestra open timer expired but the door is CLOSED. Timer should have been cancelled.