A mix. A bunch of my rule templates were written to work in both but still use the application/javascript
MIME type. I plan on upgrading those to JS Scripting rather than Nashorn with @runtime imported but haven’t gotten around to it yet and need to figure out a proper upgrade path for end users.
I have 51 rules and 21 scripts. I have 15 script actions/conditions that are application/javascript
and 64 script actions/conditions that are application/javascript;version=ECMAScript-2021
and a smattering of rules that are straight UI.
All rules are in the UI.
A few of my rules run at least once every few seconds. After the initial errors during startup they run smoothly and without error of delay.
For example, here are a few events.log entries for my humidifier_threshold rule which is pretty complex running in Nashorn.
2022-12-28 10:11:05.695 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:11:05.695 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:11:07.288 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:11:07.289 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:11:31.506 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:11:31.506 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:11:43.584 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:11:43.584 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:12:01.864 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:12:01.865 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:12:19.911 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:12:19.913 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:12:21.925 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:12:21.926 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:12:32.039 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:12:32.040 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:13:10.357 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:13:10.357 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
2022-12-28 10:13:14.378 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: RUNNING
2022-12-28 10:13:14.378 [INFO ] [openhab.event.RuleStatusInfoEvent ] - humidity_threshold updated: IDLE
And the code:
configuration:
comparison: <
dnd_end: 07:00
limit: 4h
threshold: 35
script: humidity_alert
dnd_start: 22:00
group: MinIndoorHumidity
triggers:
- id: "1"
configuration:
itemName: MinIndoorHumidity
type: core.ItemStateChangeTrigger
conditions:
- inputs: {}
id: "2"
configuration:
itemName: MinIndoorHumidity
state: 25 %
operator: <
type: core.ItemStateCondition
actions:
- inputs: {}
id: "3"
configuration:
type: application/javascript
script: >
if(typeof(require) === "function") Object.assign(this,
require('@runtime'));
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Threshold MinIndoorHumidity");
var ZonedDateTime = Java.type("java.time.ZonedDateTime");
this.logger.debug("Processing initialization data");
var group = "MinIndoorHumidity";
var limit = "24h";
var alertScript = "humidity_alert";
var filterFunc = function(i) { return i.state.class !== UnDefType.class && i.state.floatValue() < 25.0; };
var dndStartStr = "22:00";
var dndStartStrs = dndStartStr.split(":");
var dndStart = ZonedDateTime.now()
.withHour(parseInt(dndStartStrs[0]))
.withMinute(parseInt(dndStartStrs[1]))
.withSecond(0)
.withNano(0);
var dndEndStr = "07:00";
var dndEndStrs = dndEndStr.split(":");
var dndEnd = ZonedDateTime.now()
.withHour(parseInt(dndEndStrs[0]))
.withMinute(parseInt(dndEndStrs[1]))
.withSecond(0)
.withNano(0);
this.logger.debug("Done, determining if it's time to call the script");
// TODO Move to library when able
/**
var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/community/rateLimit.js');
load(OPENHAB_CONF+'/automation/lib/javascript/personal/alerting.js');
load(OPENHAB_CONF+'/automation/lib/javascript/personal/metadata.js');
*/
/**
* A class that will limit how often an event can occur. One calls run and pass
* a time_utils when to indicate how long before the call to run will run again.
* If run is called before that amount of time then the call is ignored.
*/
var RateLimit = function() {
'use strict';
// var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
this.ZonedDateTime = Java.type("java.time.ZonedDateTime");
this.log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.RateLimit");
this.log.debug("Building the RateLimit object.");
// load(OPENHAB_CONF+'/automation/lib/javascript/community/timeUtils.js');
this.until = this.ZonedDateTime.now().minusSeconds(1);
this.log.debug("RateLimit is ready to operate");
}
// reproduced from timeUtils, truncated to only parse duration strings
RateLimit.prototype.toDateTime = function(when) {
var Duration = Java.type("java.time.Duration");
var ZonedDateTime = Java.type("java.time.ZonedDateTime");
var dur = null;
var dt = null;
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(when))) {
this.log.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){
this.log.warn("Could not parse any time information from '" + timeStr +"'. Examples of valid string: '8h', '2d8h5s200z', '3d 7m'.");
}
else {
this.log.debug("Days = " + params["d"] + " hours = " + params["h"] + " minutes = " + params["m"] + " seconds = " + params["s"] + " msec = " + params["z"]);
dur = Duration.ofDays(params["d"]).plusHours(params["h"]).plusMinutes(params["m"]).plusSeconds(params["s"]).plusMillis(params["z"]);
}
if(dur !== null) {
dt = ZonedDateTime.now().plus(dur);
}
return dt;
},
/**
* Function called to attempt to run the passed in function. If enough time has
* passed since the last time run was called func is called. If not the call is
* ignored.
*
* @param {function} func called if it's been long enough since the last call to run
* @param {*} when any of the durations supported by time_utils.
*/
RateLimit.prototype.run = function(func, when){
ZonedDateTime = Java.type("java.time.ZonedDateTime");
var now = ZonedDateTime.now();
if(now.isAfter(this.until)) {
this.log.debug("It has been long enough, running the function");
this.until = this.toDateTime(when);
func();
}
else {
this.log.debug("It is still too soon, not running the function");
}
}
// END TODO
var alertGenerator = function(nullItems, nullItemLabels, threshItems, threshItemLabels, filterFunc){
return function(){
// Only call the rule if the Group still doens't meet the criteria.
// It's possible that the function is delayed some hours before it runs and the threshold may no longer be a problem.
if(filterFunc(ir.getItem(group))) {
// Get the RuleManager
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var ScriptHandler = Java.type("org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler");
var _bundle = FrameworkUtil.getBundle(ScriptHandler.class);
var bundle_context = _bundle.getBundleContext();
var classname = "org.openhab.core.automation.RuleManager";
var RuleManager_Ref = bundle_context.getServiceReference(classname);
var RuleManager = bundle_context.getService(RuleManager_Ref);
var map = new java.util.HashMap();
map.put("nullItems", nullItems);
map.put("nullItemLabels", nullItemLabels);
map.put("threshItems", threshItems);
map.put("threshItemLabels", threshItemLabels);
RuleManager.runNow(alertScript, true, map);
}
else {
this.logger.info("No longer violating threshold");
}
}
}
var getLabels = function(list) {
this.logger.debug("Getting the labels");
var Collectors = Java.type("java.util.stream.Collectors");
return list.stream()
.map(function(i) { return i.label; })
.collect(Collectors.joining(", "));
}
var getItems = function(group, filterFunc) {
this.logger.debug("Getting the Items");
var Collectors = Java.type("java.util.stream.Collectors");
return ir.getItem(group)
.members
.stream()
.filter(filterFunc)
.collect(Collectors.toList());
}
var callGenerator = function(group, filterFunc) {
this.logger.debug("Generating the script calling function");
return function() {
this.logger.debug("Calling the script");
// TODO Expand to handle non-numeric states
var nullItems = getItems(group,function(i) { return i.state.class == UnDefType.class; });
var nullItemLabels = getLabels(nullItems);
var threshItems = getItems(group, filterFunc);
var threshItemLabels = getLabels(threshItems);
this.rl.run(alertGenerator(nullItems, nullItemLabels, threshItems, threshItemLabels, filterFunc), limit);
this.timer = undefined;
}
}
this.rl = (this.rl === undefined) ? new RateLimit(): this.rl;
var now = ZonedDateTime.now();
// DND spans midnight, move end time to tomorrow if we are before midnight
if(dndEnd.isBefore(dndStart) && now.isAfter(dndStart)) {
// Move dndEnd to tomorrow
this.logger.debug("Moving end time to tomorrow");
var tomorrow = now.plusDays(1);
dndEnd = dndEnd.withYear(tomorrow.getYear())
.withMonth(tomorrow.getMonthValue())
.withDayOfMonth(tomorrow.getDayOfMonth())
.withZoneSameLocal(tomorrow.getOffset());
}
// if DND spans midnight, move the start time to yesterday if we are after midnight
else if(dndEnd.isBefore(dndStart) && now.isBefore(dndStart)) {
// Move dndStart to yesterday
this.logger.debug("Moving start time to yesterday");
var yesterday = now.minusDays(1);
dndStart = dndStart.withYear(yesterday.getYear())
.withMonth(yesterday.getMonthValue())
.withDayOfMonth(yesterday.getDayOfMonth())
.withZoneSameLocal(yesterday.getOffset());
}
this.logger.debug("Now: " + now + " Start DND Time: " + dndStart + " End DND Time: " + dndEnd);
var isDnd = now.isAfter(dndStart) && now.isBefore(dndEnd);
this.logger.debug("Is DND = " + isDnd);
// Immediately call when no delay is required
if(dndStartStr == dndEndStr || !isDnd) {
this.logger.debug("Not during a DND time, calling script immediately");
callGenerator(group, filterFunc)();
}
// Schedule a timer to run at the end of the dnd time
else if(this.timer === undefined){
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
this.logger.debug("Creating a timer to call the script after the DND time.");
this.timer = ScriptExecution.createTimer(dndEnd, callGenerator(group, filterFunc));
}
else {
this.logger.debug("Timer already exists to call the script.");
}
type: script.ScriptAction
See details at Threshold Alert [3.2.0;3.4.0).
Here are some logs for a similar rule in JS Scripting.
2022-12-28 10:16:52.343 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: RUNNING
2022-12-28 10:16:52.343 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: IDLE
2022-12-28 10:16:52.344 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: RUNNING
2022-12-28 10:16:52.345 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: IDLE
2022-12-28 10:17:03.096 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: RUNNING
2022-12-28 10:17:03.097 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: IDLE
2022-12-28 10:17:05.119 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: RUNNING
2022-12-28 10:17:05.120 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: IDLE
2022-12-28 10:17:07.137 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: RUNNING
2022-12-28 10:17:07.139 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: IDLE
2022-12-28 10:17:11.174 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: RUNNING
2022-12-28 10:17:11.176 [INFO ] [openhab.event.RuleStatusInfoEvent ] - sensor-offline-detection updated: IDLE
and the code
configuration:
dndEnd: 08:00
reschedule: false
defaultTimeout: PT5m
invert: true
alertRuleUID: service_offline_proc
groupTriggers: ServiceStatuses
dndStart: 22:00
repeatPeriod: PT12h
timeoutMetadata: rem_time
alertState: OFF
triggers:
- id: "1"
configuration:
groupName: ServiceStatuses
type: core.GroupStateChangeTrigger
conditions: []
actions:
- inputs: {}
id: "2"
configuration:
type: application/javascript;version=ECMAScript-2021
script: >
var {loopingTimer, timeUtils} = require('openhab_rules_tools');
var logger = log('rules_tools.'+ruleUID);
// Rule Properties
var alertState = "OFF";
var defaultTimeout = "PT5m";
var timeoutMetadata = "rem_time";
var alertRuleUID = "service_offline_proc";
var repeatPeriod = "PT12h";
var reschedule = false;
var dndStart = "22:00";
var dndEnd = "08:00";
var noAlertPeriod = (dndStart == dndEnd);
var isAlertState = (curr) => { return (true) ? (curr == alertState) : (curr != alertState); };
var TIMERS_KEY = ruleUID+'_timers';
var TIMER_BUFFER = 600*1000000; // Nano seconds
// If you edit this rule, run it manually to clear out the cache or else
// errors will occur complaining about the context being closed.
//if(this.event === undefined) {
// logger.info('Resetting looping timers');
// cache.put(TIMERS_KEY, null);
//}
//else {
var timers = cache.private.get(TIMERS_KEY, () => new Map());
var item = event.itemName;
var state = event.itemState.toString();
// Temporary work around until PR to fix openhab-js gets merged
var isBetweenTimes = function (t, start, end) {
var startTime = time.toZDT(start).toLocalTime();
var endTime = time.toZDT(end).toLocalTime();
// const currTime = this.toLocalTime();
var currTime = time.toZDT(t).toLocalTime();
// time range spans midnight
if (endTime.isBefore(startTime)) {
return currTime.isAfter(startTime) || currTime.isBefore(endTime);
} else {
return currTime.isAfter(startTime) && currTime.isBefore(endTime);
}
};
// Looks at the currently scheduled timers and makes the time for a new one
// doesn't overlap.
// Loop through the timers and check the timeout time against the existing timers.
// If it's within half a second of one, move the timer forward 500 msec.
// If the timeout was moved at least once, loop through the timers again to make
// sure we didn't move it on top of an already checked timer.
var avoidOverlapping = (timeout) => {
let rval = time.toZDT(timeout);
let recheck = true;
while(recheck) {
recheck = false;
logger.debug('There are ' + timers.size + ' timers already scheduled');
timers.forEach((value, key) => {
if(!value.hasTerminated() &&
rval.isClose(time.toZDT(value.timer.getExecutionTime()), time.Duration.ofMillis(250))) {
logger.debug('Found timer ' + value.name + ' that has not terminated and is too close to timeout');
rval = rval.plusNanos(TIMER_BUFFER);
recheck = true;
}
});
}
return rval;
};
// Moves the alert time to the end of the DND period if the timeout is
// in the DND period. Then it makes sure it's at least TIMER_BUFFER nanosecs
// from any existing timer
var generateAlertTime = (timeout, dnds, dnde) => {
let rval = time.toZDT(timeout);
if(isBetweenTimes(rval, dnds, dnde)) { // temporary work around until PR gets merged to openhab-js
logger.debug('Timer is scheduled during DND, moving to end of DND');
rval = dndEnd;
if(time.toZDT(dndEnd).isBefore(time.toZDT())) {
rval = timeUtils.toTomorrow(dndEnd);
}
}
return avoidOverlapping(rval);
};
// This returns the function called by the LoopingTimer. LoopingTimer will
// reschedule the timer based on what the function passed to it returns. If
// a duration is returned it will be reschuled for that time into the future.
// If null is returned the looping stops.
var sendAlertGenerator = (alertItem, repeat, dnds, dnde) => {
return () => {
const currState = items.getItem(alertItem).state;
logger.debug('Timer expired for ' + alertItem + ' with dnd between ' + dnds + ' and ' + dnde);
// Calculate the next repeat time
let repeatTime = (repeat) ? generateAlertTime(repeat, dnds, dnde) : null;
// Send alert
if(isAlertState(currState)) {
logger.debug('Item is still in an alert state.')
rules.runRule(alertRuleUID, { 'alertItem': alertItem, 'currState': currState }, true);
}
// Cancel the repeats
else {
logger.debug('Item has returned to the non-alert state');
repeatTime = null;
}
// If there is no repeat delete the timer from the map
if(!repeatTime) {
logger.debug('No repeat time');
timers.delete(alertItem);
}
logger.debug('Rescheduling for ' + repeatTime);
return repeatTime;
};
};
logger.debug('Running door alert rule with: \n' +
' item: ' + item + '\n' +
' state: ' + state + '\n' +
' alertState: ' + alertState + '\n' +
' invert: ' + true + '\n' +
' defaultTimeout: ' + defaultTimeout + '\n' +
' repeatPeriod: ' + repeatPeriod + '\n' +
' dndStart: ' + dndStart + '\n' +
' dndEnd: ' + dndEnd + '\n' +
' noAlertPeriod: ' + noAlertPeriod + '\n' +
' alertRuleUID: ' + alertRuleUID);
// Returned to alertState and timer exists, cancel the looping timer
if(!isAlertState(state) && timers.has(item)) {
logger.debug(item + ' has returned to a non-alerting state of ' + state + ', canceling the timer if it exists.');
timers.get(item).cancel();
timers.delete(item);
}
// Item changed to a not alertState, create a looping timer if one doesn't already exist
else if(isAlertState(state)) {
logger.debug(item + ' is in the alert state of ' + state);
// There shouldn't be a Timer if the Item just changed to the alertState, log to show
// something went wrong.
if(timers.has(item)) {
logger.warn(item + ' state is now ' + state + ' but an alert timer already exists! This should not have happened!');
}
// Schedule a looping timer to start at the initial timeout (from metadata) and repeat
// on repeatPeriod
else {
const metadata = items.getItem(item).getMetadataValue(timeoutMetadata);
let timeout = generateAlertTime((metadata) ? metadata : defaultTimeout, dndStart, dndEnd);
timers.set(item, new loopingTimer.LoopingTimer()); // add name as argument when openhab_rules_tools is updated to support that
logger.debug('Starting timer for ' + item + ' with ' + timeout);
timers.get(item).loop(sendAlertGenerator(item, repeatPeriod, dndStart, dndEnd), timeout);
}
}
//}
type: script.ScriptAction
See Open Reminder [3.3.0;3.4.0) for details.
In both cases they pretty consistently take 1 msec to run even from the initial start of OH.
I’m not sure how to show the heap space in use by Java. I’ve never personally encountered a memory leak in OH so never learned. I run OH in Docker on an Ubuntu VM and it currently is sitting at 5883M VIRT, 2225M RES and 25576M SHR (according to htop
). But the Docker container explodes the memory requirements. I’ve tried to ruin jsstat
but get an illegal argument exception. I think the container isolation interferes with jsstat.