Some JS Scripting UI Rules Examples

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.

15 Likes