Debounce [3.2.0;3.4.9]

Done. That post is very old and in an initial implementation it was called “state” instead of “states”.

Thx for the fast reply, works for me perfectly.

For my rule I added some debug logging:

else {
  logger.debug(event.itemName + " changed to " + event.itemState + " which is not debouncing");
  if (this.timers.hasTimer(event.itemName)) {
    logger.debug("cancelling timer for " + event.itemName);
    this.timers.cancel(event.itemName);
  }
  end_debounce_generator(event.itemState, cfg["proxy"], cfg["command"])();
}

Thx again!

Two minor suggestions:

  1. Step-by-step clarification

Should probably be

In the UI, navigate to the “raw” Item, click on “Add Metadata”, click “Enter Custom Namespace…” (under “Custom namespaces” section) and enter “debounce” as the namespace.


  1. Stylistic suggestion: separate the two config examples code blocks
Switch RawSensor (Debounce) { debounce="ProxySensor"[timeout="2m", states="OFF", command="False"] }
Switch ProxySensor

or in the UI

value: ProxySensor
config:
  states: OFF
  timeout: 2m
  command: "True"

Hmm I would like to several debounce rules configured, many of them operating on a single “raw” item.

I am thinking of I really need several “debounce group items” or could I simply slap all the raw items into single group?

I have actually currently pointed the rules towards the raw items (instead of a group). Getting some funky behavior…Now I realize that will not obviously work since “member of a group” trigger has been used in the rule.

I’m OK with that and will change it in the OP.

I agree with the suggestion but the code that imports it and shows it in MainUI wasn’t able to handle two code blocks so they are combined into one.

Not that there’s any problem with that approach but is there is a reason why?

You are freely able to modify the rule generated by the template in any way you need to. It’d only take a minor edit to the rule to change the trigger to an Item based trigger instead of Member of trigger.

1 Like

Simply…just trying to avoid having so many items, it makes it harder to understand the system later on (like 1y later when I have forgotten all this).

Similarly, I avoid modifying templated rules so I can update them without worrying what changes to make where.

For what it’s worth, having single debounce group seems to work fine based on quick testing. Quick code review suggests that rule uses member item name and member item state, and the actual group acts almost as filter mechanism for events only. The group name and group state (if any) does not seem play a rule, at least with the current version.

This is great as I also avoid having multiple debounce rules, making it easier to update the rule when needed.


Thanks for this, I find it very useful. I use this to debounce raw status that has been formed from item state (looking at undef) and thing status (online/offline) (Device status online monitor). With “expire” I can pick up nonupdating zwave items.

I had setup alerting first without debounce but that is suspect to some transient unnecessary alerts since my modbus device is sometimes unable to respond to requests (like, for 20 seconds). Debounce will resolve this kind of online/offline flakiness and alert only if the issue remains.

Correct. The Group name serves only configure the trigger for the rule. The event is used to get the Item’s name and state which will be the same whether the rule is triggered by Member of or Item received triggers.

As you’ve seen you really only need the one Group Item in addition to your already existing Items and that Group’s sole purpose is to trigger the rule.

Perhaps someday if a rule’s triggers are easier to modify at runtime I’ll change it so the rule automatically gets triggered by Items with the debounce metadata (that’s how my original Jython version worked) but there are weird limitations with rules creating rules in the UI that makes that challenging.

I use it for my persistence (have to be away for five minutes before I actually count it as away) and for a few wired reed sensors that go crazy periodically when the temp gets below 10 degrees F. Even just a half second debounce on those is enough to go for dozens of alerts to none (because the door never actually opened in the first place.

There was a PR and issue opened to create a Debounce profile but I don’t know that it’s ever been completed or merged. That wouldn’t completely replace this rule template since profiles only work with Links and not all Items one may want to debounce have a Link (e.g. the Item that is debounced for my presence detection is actually a Group Item with an aggregation function).

I’m glad you find it useful.

Thanks!

1 Like

I solved the debouncing pattern with the persistence service. Imagine this: you have a CO2 sensor that reports a value every minute. A rule processes this value and switches the speed of a fan at certain threshold values. Around this threshold, the fan is cycled up and down every minute. We need a debouncer. My implementation simply checks whether the value has changed for a certain period of time and if so, the fan speed is NOT updated/switched (for 30 minutes in my case).

var zdt = Java.type('java.time.ZonedDateTime');
var persistence = Java.type('org.openhab.core.persistence.extensions.PersistenceExtensions');

var co2 = itemRegistry.getItem('co2').getState().format('%f');
var co2_control = itemRegistry.getItem('co2_control').getState();

var step_changed_since_period = persistence.changedSince(itemRegistry.getItem('step'), zdt.now().plusMinutes(-30));

if (!step_changed_since_period) {
  var step = 2;
  if (co2_control === ON) {
    if (co2 >= 900) {
      step = 3;
    }
    if (co2 < 500) {
      step = 1;
    }
  }
  events.sendCommand('step', step);
}

Keep in mind that persistence is relatively slow. Anything faster than say 500 msec will run into timing problems as it takes time to save to the database and that happens in parallel with the rule execution.

Thanks for the feedback. True, depends on the use case. If the co2 sensor doesn’t report very often (everything above 5 secs is safe), this isn’t an issue. I’m using cron to trigger my script every 5 minutes, which is absolutely sufficent controlling air quality. I thought this approach might help solving simple use cases, since it is fairly easy to implement.

It might be worth posting as a separate Tutorials and Solutions thread. The discussion under a posting in the marketplace should be limited to the rule template posted in the first post. It’s not really appropriate for side discussions and alternative approaches.

Keep in mind that this thread represents a rule template that can be installed and configured through MainUI. The discussion should be related only to that.

I am experiencing initialization issues with this rule - I suspect it is a form of race condition.

Now on openHAB startup, I end up having NULL debounced item while the “input items” (items having debounce metadata) are OK/ON.

I suspect the “input items” get their state update/state change (from NULL to ON) before the rule is ready to process the update.

This is of course just a theory. Is this expected behaviour?

It would be great if the rule has on-startup behaviour to initalize the result item if we do not receive any state changes. Or perhaps there is some other way to mitigate this?

I think this would be expected. It’s pretty typical that restoreOnStartup happens before the rule engine is ready so there is no event to trigger the rule in that case.

I would expect the proxy Item to be the one with restoreOnStartup rather than or in addition to Item linked to the Channel.

However, if the Channel linked Item is indeed getting an update before the rule engine runs that one is a little tricky. IIRC the additions to event that include more information about how the rule was triggered hasn’t been merged yet making it not quite as straight forward to tell when the rule is triggered during a system start. I’ll have to think about it.

This whole rule template will be rewritten for OH 4, I’ll have to take this into account.

I do not use restoreOnStartup so it might be that rules actually take some time to initialize… more than my things that start pushing state updates

This is my workaround in Device status online monitor

const cronOrStarted = typeof event === 'undefined' || (event.statusInfo === undefined && event.itemState === undefined)

There is also the type property in UI events: JavaScript Scripting - Automation | openHAB (I have not tried this, not sure which version includes this)

The Rules Engine starts at runlevel 50 but the Thing don’t initialize until runlevel 80. But it’s not deterministic. On a fast(ish) machine I could see that this rule is still initializing after the Things have initialized. But that wouldn’t be typical.

I know there is some talk about looking at this behavior in OH 4 because of other issues so maybe this will get fixed as a matter of course.------------------------------

Yes, but that will also match when the rule is run manually or based on a time trigger or when called from another rule. I’m not sure it makes sense to blindly run the rule like it’s system started in all of those circumstances.

I wasn’t aware that PR has been merged. That makes it a little easier, though I still need to decide what makes sense for some of those other trigger types.

Hmm weird thing. I have raspi4 fwiw.

I guess logs would reveal the sequence of events but have not looked into this so deeply.

One simple workaround I can think of is having a separate rule to initialize debounced item (if state is still null). But thought it would make sense to incorporate it into this one

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.

Thanks, very much would like to try this out! Does this work in 3.3? I haven’t updated yet

Tried it but getting some errors

  • mimetype refers to old-js, changed the it to refer to new ecmascript
  • some usage message refer to “state” not “states”. Probably a typo

But still getting this:

023-01-07 13:42:05.903 [WARN ] [nhab.automation.rules_tools.Deb ounce] [org.openhab.automation.jsscripting] - Item SiemensS7Modb usHoldingOKRaw has invalid debounce metadata: {} Debounce metadata should follow this format: .items File: debounce=ProxyItem[command=true, timeout='PT2S', st ate='ON,OFF'] UI YAML: use 'debounce' for the namespace and metadata format value: ProxyItem config: command: true timeout: PT3S state: ON,OFF }; timeout must be in a format supported by time.toZDT() in the add -on's helper library.

Metadata entry

value: SiemensS7ModbusHoldingOK
config:
  timeout: PT5M
  states: OFF

That’s right. They swapped the mime types in 4 already. Now JS Scripting gets the standard and Nashorn has the non-standard one.

Not as written. It uses the new privateCache which wasn’t added until 3.4. You’ll have to change the line that instantiates the timerMgr to use just cache, not cache.private.

That error doesn’t make sense to me. I can’t remember when certain things were added to the helper library. You might need to install the openHABian node module to get the gateway version of that, though I fear that has dependencies on changes in core.

You’ll probably need to upgrade to 3 4 fire this to work.