If you would like to try it, here is a version of Debounce rewritten in JS Scripting using the helper library. It has two modes. When it’s triggered with an Item event it operates as expected.
When it’s triggered in any other way it does the following:
- pulls the Items with “debounce” metadata
- validates the metadata is usable, reporting what’s wrong with it
- updates the proxy Item with the current state of the source Item
- reports any Item which has “debounce” metadata but is not a member of the Debounce Group.
So it serves as a validity check on your Item’s configs as well as a way to initialize your proxy Items. You can add a System runlevel trigger or just run the rule manually to meet your initialization use case. I might make that an option that can be turned on and off through the rule template properties.
configuration: {}
triggers:
- id: "2"
configuration:
groupName: Debounce
type: core.GroupStateChangeTrigger
conditions:
- inputs: {}
id: "1"
label: Limit rule execution
description: Only runs rule if it's not an Item change to NULL/UNDEF or it's not
an Item event
configuration:
type: application/javascript
script: >
console.loggerName = 'org.openhab.automation.rules_tools.Debounce';
if(this.event !== undefined && this.event.itemName !== undefined){
const item = items.getItem(this.event.itemName);
if(item.isUninitialized) {
console.debug('Debounce for Item', this.event.itemName, 'is blocked for state', this.event.itemState);
}
!item.isUninitialized;
}
else {
console.trace('Debounce passed conditions');
true;
}
type: script.ScriptCondition
actions:
- inputs: {}
id: "3"
label: Debounces the state of an Item
description: Waits the configured amount of time after the most recent change
before updating or commanding a proxy Item.
configuration:
type: application/javascript
script: >-
var {timerMgr} = require('openhab_rules_tools');
console.loggerName = 'org.openhab.automation.rules_tools.Debounce';
var USAGE = "Debounce metadata should follow this format:\n"
+ ".items File: debounce=ProxyItem[command=true, timeout='PT2S', state='ON,OFF']\n"
+ "UI YAML: use 'debounce' for the namespace and metadata format\n"
+ "value: ProxyItem\n"
+ "config:\n"
+ " command: true\n"
+ " timeout: PT3S\n"
+ " state: ON,OFF\n"
+ "};\n"
+ "timeout must be in a format supported by time.toZDT() in the add-on's helper library."
/**
* Get and check the Item metadata.
* @return {dict} the metadata parsed and validated
*/
var getConfig = (itemName) => {
// ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO
// Replace with item.getMetadata() when available
// Get Metadata query stuff
const MetadataRegistry = osgi.getService('org.openhab.core.items.MetadataRegistry');
const Metadata = Java.type('org.openhab.core.items.Metadata');
const MetadataKey = Java.type('org.openhab.core.items.MetadataKey');
const md = MetadataRegistry.get(new MetadataKey('debounce', itemName));
if(!md) {
throw itemName + ' does not have debounce metadata!\n' + USAGE;
}
if(!md.value) {
throw itemName + ' has malformed debounce metadata, no value found!\n' + USAGE;
}
if(!items.getItem(md.value)) {
throw itemName + ' has invalid debounce metadata, proxy Item ' + md.value + ' does not exist!\n' + USAGE;
}
if(!md.configuration['timeout']) {
throw itemName + 'has malformed debounce metadata, timeout configuration parameter does not exist!\n' + USAGE;
}
if(!time.toZDT(md.configuration['timeout'])) {
throw itemName + ' has invalid debounce metadata, timeout ' + md.configuration['timeout'] + ' cannot be parsed to a valid duration!\n' + USAGE;
}
var cfg = {'proxy': md.value,
'timeout': md.configuration['timeout'],
'command': 'command' in md.configuration && md.configuration['command'].toString().toLowerCase() == 'true',
'states': [],
};
const stateStr = md.configuration['states'];
if(stateStr) {
stateStr.split(',').forEach((st) => {
cfg.states.push(st.trim());
});
}
return cfg;
};
/**
* Called at the end of a debounce to update or command the proxy Item
* @param {string} name the originating Item's name
* @param {string} state the originating Item's new state
* @param {string} proxy the name of the proxy Item
* @param {boolean} isCommand whether to send a command or update to the proxy
* @return {function} an argumentless function to call to update/command the proxy Item at the end of the debounce
*/
var endDebounceGenerator = (name, state, proxy, isCommand) => {
return function(){
console.debug('End debounce for', name, "state", state, 'with proxy', proxy, 'and isCommand', isCommand);
const isCurrState = (items.getItem(name).state == state)
if(isCommand && !isCurrState) {
console.trace('Commanding');
items.getItem(proxy).sendCommand(state);
}
else if(!isCommand && !isCurrState) {
console.trace('Updating');
items.getItem(proxy).postUpdate(state);
}
};
};
var debounce = () => {
console.debug('Debounce:', event.type, 'item:', event.itemName);
// Initialize the timers, congif and end debounce function
const timers = cache.private.get('timerMgr', () => new timerMgr.TimerMgr());
const cfg = getConfig(event.itemName);
const endDebounce = endDebounceGenerator(event.itemName, event.itemState, cfg.proxy, cfg.command);
// If there are no states in the debounce metadata or if the new state is in the list of debounce states
// set a timer based on the timeout parameter
if(cfg.states.length == 0
|| cfg.states.includes(event.itemState.toString())) {
console.debug('Debouncing ', event.itemName, "'s state", event.itemState,
'using proxy', cfg.proxy, 'timeout', cfg.timeout,
'command ', cfg.command, 'and states ', cfg.states);
timers.check(event.itemName, cfg.timeout, endDebounce);
}
// If this is not a debounced state, immediately forward it to the proxy Item
else {
console.debug(event.itemName, 'changed to', event.itemState, 'which is not among the debounce states:', cfg.states);
timers.cancel(event.itemName);
endDebounce();
}
};
var init = () => {
console.info("Validating Item metadata, group membership, and initializing the proxies");
let isGood = true;
let badItems = [];
// Get all the Items with debounce metadata and check them
const is = items.getItems();
console.debug('There are', is.length, 'Items');
const filtered = is.filter( item => item.getMetadataValue('debounce'));
console.debug('There are ', filtered.length, 'Items with debounce metadata');
filtered.forEach(item => {
console.debug('Item', item.name, 'has debounce metadata');
try {
const cfg = getConfig(item.name);
const proxy = items.getItem(cfg.proxy);
if(proxy.state != item.state) {
console.info('Updating', cfgProxy, 'to', item.state);
proxy.postUpdate(item.state);
}
else {
console.debug(cfg.proxy, 'is already in the state of', item.state);
}
if(!item.groupNames.includes('Debounce')) {
console.warn(item.name, 'has debounce metadata but is not a member of Debounce!')
isGood = false;
badItems.push(item.name);
}
}
catch(e) {
console.warn('Item', item.name, 'has invalid debounce metadata:\n', e, '\n', USAGE);
isGood = false;
badItems.push(item.name);
}
});
// Report those Items that are members of Debounce but don't have debounce metadata
items.getItem('Debounce').members.filter(item => {!item.getMetadataValue('debounce')}).forEach(item => {
console.warn(item.name, 'is a member of Debounce but lacks debounce metadata');
isGood = false;
badItems.push(item.name);
});
if(isGood) {
console.info('All debounce Items are configured correctly');
}
else{
console.log('The following Items have an invalid configuration. See above for details:', badItems);
}
}
// ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO ~ TODO
// change when there is always an event object
if(this.event === undefined) {
init();
}
else {
switch(event.type) {
case 'ItemStateEvent':
case 'ItemStateChangedEvent':
case 'ItemCommandEvent':
debounce();
break;
default:
init();
}
}
type: script.ScriptAction
This version requires Announcing the initial release of openhab_rules_tools which can be installed through openHABian or manually from the command line. New versions of openHABian will install it by default.
It’s not been made into a rule template yet. For now it should work in OH 3.4. But soon I’ll adopt some of the new goodness coming down the line (event.type to tell all the ways a rule is triggered, better ways to get the Item metadata) and it will only support OH 4.0 and later. At that point I’ll probably create a new post to the Marketplace that only supports OH 4.0+ and leave this one for legacy support.
Note, the timeout
property has changed to only support ISO8601 formatted durations but they have also been expanded to support other stuff (e.g. number of milliseconds). That will be the first thing you see when you run it manually.
Installation:
- install the JS Scripting add-on
- create a new rule, fill out the metadata (UID, name, description, etc.)
- copy the code above and paste it into the code tab of the rule
- since there’s no properties you might need to do a find and replace for the Group name and debounce metadata namespace you used if it’s not “Debounce” and “debounce” respectively.
Even if you don’t try it out, let me know based on the description if I’m heading in the right direction.
One of the things I want to do with all of my rule templates is improve that initial error checking of the metadata (where applicable) in a similar way.