Design Pattern: Debounce

Edit: Rewritten for OH 4.

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for a description of DPs.

Problem Statement

There are sometimes cases where one may want to ignore events that occur immediately after an initial event for a time, waiting until the state settles. For example, in electronics when a button is pressed, the spring that pushes up the button will bounce around potentially registering multiple button presses. To ignore those bounce events is caused “debouncing”.

This can be used in areas outside of push buttons.

Debounce is very similar to Design Pattern: Gate Keeper and Design Pattern: Rate Limit. The differences are:

  • Gatekeeper processes every event with a defined spacing between the processing of each event.
  • Rate Limit only processes the first event and throws away all events that occur for a time after that first event.
  • Debounce only processes the last event when it remains in that last state for a given time, throwing away the events that happened before that point.

Rule Template

There is a rule template on the marketplace that implements Debounce.

  1. Create a proxy Item for each Item you want to debounce.

  2. Create a Group Item I’ll call “Debounce” and add all the “raw” Items (i.e. the Items linked to the Channels that need to be debounced) as members.

  3. Add Item metadata to each of the raw Items. The namespace is can be anything, I’ll use “debounce” here. The “value” is the name of the corresponding proxy Item. The configuration supports states in case you only want to debounce one state but not all states, timeout which says how long after that last update to wait before accepting it as the debounce state, and a flag to command the proxy Item instead of update it. See the rule template docs for details.

  4. Go to Add-on Store → Automation → Rule Templates and install Debounce.

  5. Go Settings → Rules → + to create a new rule and select “Debounce” under “Create from Template”. Set the properties as:

Property Value Purpose
Rule ID Something meaningful Unique identifier for the rule
Name Something meaningful Name the rule appears under
Description Something meaningful A sentence or two describing what the rule does.
Debounce Group Debounce Select the Group created in 2 above. This is used to trigger the rule.
Debounce Metadata Namespace debounce The name of the Item metadata namespace to use as configuration for that Item
Initialize Proxies toggled on When on it causes the rule to initialize all the proxy Items with the current state of the raw Items when the rule is triggered manually (e.g. run manually).
6 Likes

Hi @rlkoshak,

I tried to implement your debounce pattern by using the JS through the MainUI, but unfortunately I failed. Below some points that I found:

  • debounce.yml in the repository is not a yml, but just JS
  • state should be states in line 21+ 38
  • end_debounce should be end_debounce_generator in line 82
  • return function() in line 58 did not work in my case. I removed the function and moved the return; after the command/postUpdate

After those changes, the debouncing is still not working. The Presence Item is set to OFF immediately.

2020-12-11 10:44:31.976 [INFO ] [.openhab.model.script.Rules.Debounce] - PresenceSensors changed to ON with is not debouncing
2020-12-11 10:44:31.977 [INFO ] [.openhab.model.script.Rules.Debounce] - End debounce for Presence new state = ON curr state = OFF
2020-12-11 10:44:34.157 [INFO ] [.openhab.model.script.Rules.Debounce] - Debouncing PresenceSensors with proxy = Presence timeout = 1m and states = OFF
2020-12-11 10:44:34.164 [INFO ] [.openhab.model.script.Rules.Debounce] - End debounce for Presence new state = OFF curr state = ON

Weird, I must have copied wrong. It’s supposed to be the full YAML. I’ll get that fixed as soon as I get to a computer. It’s stuff like this that I filed an issue to have a file import/export instead of copy and paste.

No, state is correct. When I first wrote the Python version where I defined the metadata it only supported one state. I’ve since expanded to to support more than one state but to avoid breaking things I kept the metadata unchanged. You can see on line 38 that I pull the metadata using “state”, not “states”.

Therefore, the error message on line 21 is correct. The metadata should use “state” not “states”.

Actually it should be end_debounce_generator(arg, arg, arg)(). Clearly I didn’t test the case where the state isn’t debounced. :frowning:

Didn’t work how? The function is written in that way on purpose to solve a problem with scope. end_debounce_generator returns a function() because the Timer expects a fucntion(). But if you create an anonymous function (e.g. function() { end_debounce(arg1, arg2, arg3); } ), arg1, arg2, and arg3 do not get fixed to be the values they had at the time the anonymous function was created. Instead they will have the values the last time the rule ran. So I call a function to create the function() so that the argument values get fixed and remain the values that they were when the function was generated so they don’t get overwritten by later runs of the rule.

I’m not surprised as some of your fixes were incorrect fixes.

Try the following YAML:

triggers:
  - id: "1"
    configuration:
      groupName: Debounce
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        var logger =
        Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Debounce");


        // Get Metadata query stuff

        this.FrameworkUtil = (this.FrameworkUtil === undefined) ? Java.type("org.osgi.framework.FrameworkUtil") : this.FrameworkUtil;

        this._bundle = (this._bundle === undefined) ? FrameworkUtil.getBundle(scriptExtension.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

        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');


        /**
         * Get and check the item metadata.
         * @return {dict} The metadata parsed and validated
         */
        var checkMetadata = function(itemName) {
          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(cfg.configuration["timeout"] == undefined || cfg.configuration["timeout"] === null) {
            throw itemName + " does not have a timeout parameter defined! " + USAGE;
          }
          var dict = {"proxy": cfg.value,
                      "timeout": cfg.configuration["timeout"],
                      "command": "command" in cfg.configuration && cfg.configuration["command"].toLowerCase() == "true",
                      };
                      
          var stateStr = cfg.configuration["state"];
          if(stateStr === undefined || stateStr === null) {
            throw itemName + " does not have proper debounce metadata " + cfg.toSTring;
          }
          var split = stateStr.split(",");
          dict["states"] = [];
          for(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.info("End debounce for " + proxy + " new state = " + state + " curr state = " + items[proxy]);
                if(isCommand && items[proxy] != state) {
                  events.sendCommand(proxy, state);
                }
                else if (items[proxy].toString != state) {
                  events.postUpdate(proxy, state);
                }
              }
        }


        this.timers = (this.timers === undefined) ? new TimerMgr() : this.timers;

        var cfg = checkMetadata(event.itemName);


        if(cfg["states"].length == 0 || 
          (cfg["states"].length > 0 && cfg["states"].indexOf(event.itemState.toString()) >= 0)) {
          logger.info("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.info(event.itemName + " changed to " + event.itemState + " which is not debouncing");
          end_debounce_generator(event.itemState, cfg["proxy"], cfg["command"])();
        }
    type: script.ScriptAction

Note, I’ve moved the logging to info instead of debug for now. I did a couple of tests including testing the not/debounced state and it seems to work.

But if you used “states” in your metadata, you’ll need to change them to “state”.

No, state is correct

I’m sorry I misunderstood. Based on the example above (the python version) I thought it should be “states”. Also I saw the .py says “states” in the error message
Are I’m right you are using different namespaces for python and javascript?
Within line 43+45+72+73 (debouce.yml/js) i also saw “states”.

The code in the function was not executed. However with the latest code its working, so forget about it.

Thank you Rich, with this yaml it is working!

Hmmmm. The Python version must be wrong. :smiley: Bugs all over the place.

thanks for letting me know it works. I’ll look at that Python message too. Maybe I’m just all messed up. I’m moving between OH 2.5 and OH 3 at the same time as rewriting these so mistakes are bound to happen.

Since I’m convinced, debouncing is a widely needed feature and maybe worth been implemented as early as possible in the event processing, I recently suggested to introduce an additional “debouncing link profile” in openhab-core:

Just wanted to share this with you as well as invite you for sharing your thoughts on the topic there…

1 Like

I commented on the issue. However, your use case that you outlined as a justification is already better handled by the hysteresis profile.