uid: rules_tools:debounce label: Debounce description: Waits a configured amount of time before forwarding an Item's state change to a proxy Item. configDescriptions: - name: debounceGroup type: TEXT context: item filterCriteria: - name: type value: Group label: Debounce Group required: true description: Group Item that holds the raw Items to debounce triggers: - id: "1" configuration: groupName: "{{debounceGroup}}" type: core.GroupStateChangeTrigger conditions: - inputs: {} id: "2" label: Only debounce non NULL and UNDEF states configuration: type: application/javascript script: >- if(typeof(require) === "function") Object.assign(this, require('@runtime')); var type = (typeof(require) === "function") ? UnDefType : UnDefType.class; event.itemState.class != type; type: script.ScriptCondition 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.Rules.rules_tools.Debounce"); // Get Metadata query stuff this.FrameworkUtil = (this.FrameworkUtil === undefined) ? Java.type("org.osgi.framework.FrameworkUtil") : this.FrameworkUtil; this.ScriptHandler = Java.type("org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler"); this._bundle = (this._bundle === undefined) ? FrameworkUtil.getBundle(ScriptHandler.class) : this._bundle; this.bundle_context = (this.bundle_context === undefined) ? this._bundle.getBundleContext() : this.bundle_context; this.MetadataRegistry_Ref = (this.MetadataRegistry_Ref === undefined) ? bundle_context.getServiceReference("org.openhab.core.items.MetadataRegistry") : this.MetadataRegistry_Ref; this.MetadataRegistry = (this.MetadataRegistry === undefined) ? bundle_context.getService(MetadataRegistry_Ref) : this.MetadataRegistry; this.Metadata = (this.Metadata === undefined) ? Java.type("org.openhab.core.items.Metadata") : this.Metadata; this.MetadataKey = (this.MetadataKey === undefined) ? Java.type("org.openhab.core.items.MetadataKey") : this.MetadataKey; // Load TimerMgr // TODO: Replace with an installed library when the marketplace supports it //this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getenv("OPENHAB_CONF") : this.OPENHAB_CONF; //load(this.OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js'); var TimerMgr = function() { var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF"); this.log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.rules_tools.TimerMgr"); this.log.debug("Building timerMgr instance."); this.timers = {}; // this.log.debug("Loading timeUtils"); // load(OPENHAB_CONF+'/automation/lib/javascript/community/timeUtils.js'); this.ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution"); this.log.debug("Timer Mgr is ready to operate"); } // reproduced from timeUtils, truncated to only parse duration strings TimerMgr.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; }, TimerMgr.prototype._notFlapping = function(key) { this.log.debug("Timer expired for " + key); if (key in this.timers && "notFlapping" in this.timers[key]) { this.log.debug("Calling expired function " + this.timers[key]["notFlapping"]); this.timers[key]["notFlapping"](); } if (key in this.timers){ this.log.debug("Deleting the expired timer"); delete this.timers[key]; } }, TimerMgr.prototype._noop = function() { }, TimerMgr.prototype.check = function(key, when, func, reschedule, flappingFunc) { this.log.debug("Timer manager check called"); if (reschedule === undefined) reschedule = false; var timeout = this.toDateTime(when); this.log.debug("Timer to be set for " + timeout.toString()); // Timer exists if (key in this.timers){ if (reschedule){ this.log.debug("Rescheduling timer " + key + " for " + timeout.toString()); this.timers[key]["timer"].reschedule(timeout); } else { this.log.debug("Cancelling timer " + key); this.cancel(key); } if (flappingFunc !== undefined){ this.log.debug("Running flapping function for " + key); flappingFunc(); } } // Timer doesn't already exist, create one else { this.log.debug("Creating timer for " + key); var timer = this.ScriptExecution.createTimerWithArgument(timeout, this, function(context) { context._notFlapping(key); }); this.timers[key] = { "timer": timer, "flapping": flappingFunc, "notFlapping": (func !== undefined) ? func : this._noop } this.log.debug("Timer created for " + key); } }, TimerMgr.prototype.hasTimer = function(key) { return key in this.timers; }, TimerMgr.prototype.cancel = function(key) { if (key in this.timers) { this.timers[key]["timer"].cancel(); delete this.timers[key]; } }, TimerMgr.prototype.cancelAll = function() { for (var key in this.timers) { if (!this.timers[key]["timer"].hasTerminated()) { this.log.debug("Timer has not terminated, cancelling timer " + key); this.cancel(key); } delete this.timers[key]; this.log.debug("Timer entry has been deleted for " + key); } } // TODO: End /** * Get and check the item metadata. * @return {dict} The metadata parsed and validated */ var checkMetadata = function(itemName, timers) { var USAGE = "Debounce metadata should follow debounce=ProxyItem[command=true, timeout='2s', state='ON,OFF']." var cfg = MetadataRegistry.get(new MetadataKey("debounce", itemName)); if(cfg === null) { throw itemName + " does not have debounce metadata! " + USAGE; } if(cfg.value === undefined || cfg.value === null) { throw itemName + " does not have a proxy Item defined! " + USAGE; } if(items[cfg.value === undefined]) { throw "Proxy Item " + cfg.value + " does not exist! " + USAGE; } if(cfg.configuration["timeout"] == undefined || cfg.configuration["timeout"] === null) { throw itemName + " does not have a timeout parameter defined! " + USAGE; } if(timers.toDateTime(cfg.configuration["timeout"]) === null) { throw itemName + "'s timeout " + cfg.configuration["timeout"] + " is not valid! " + USAGE; } var dict = {"proxy": cfg.value, "timeout": cfg.configuration["timeout"], "command": "command" in cfg.configuration && cfg.configuration["command"].toString().toLowerCase() == "true", }; dict["states"] = []; var stateStr = cfg.configuration["states"]; if(stateStr !== undefined && stateStr !== null) { var split = stateStr.split(","); for(var st in split) { dict["states"].push(split[st]); } } return dict; } /** * Called when the debounce timer expires, transfers the current state to the * proxy Item. * @param {string} state the state to transfer to the proxy Item * @param {string} name of the proxy Item * @param {Boolean} when true, the state is sent as a command */ var end_debounce_generator = function(state, proxy, isCommand) { return function() { logger.debug("End debounce for " + proxy + ", new state = " + state + ", curr state = " + items[proxy] + ", command = " + isCommand); if(isCommand && items[proxy] != state) { logger.debug("Sending command " + state + " to " + proxy); events.sendCommand(proxy, state.toString()); } else if (items[proxy] != state) { logger.debug("Posting update " + state + " to " + proxy); events.postUpdate(proxy, state.toString()); } } } this.timers = (this.timers === undefined) ? new TimerMgr() : this.timers; var cfg = checkMetadata(event.itemName, this.timers); if(cfg["states"].length == 0 || (cfg["states"].length > 0 && cfg["states"].indexOf(event.itemState.toString()) >= 0)) { logger.debug("Debouncing " + event.itemName + " with proxy = " + cfg["proxy"] + " timeout = " + cfg["timeout"] + " and states = " + cfg["states"]); this.timers.check(event.itemName, cfg["timeout"], end_debounce_generator(event.itemState, cfg["proxy"], cfg["command"])); } else { logger.debug(event.itemName + " changed to " + event.itemState + " which is not debouncing"); this.timers.cancel(event.itemName); // Cancel the timer if it exists end_debounce_generator(event.itemState, cfg["proxy"], cfg["command"])(); } type: script.ScriptAction label: Debounce state based on debounce metadata description: "Expected Metadata: debounce=ProxyItem[timeout='2s', state='OFF', command=true]"