ccutrer / openhab-jrubyscripting

JRuby Libraries for Openhab
Eclipse Public License 2.0
1 stars 1 forks source link

Debounce #129

Closed jimtng closed 1 year ago

jimtng commented 1 year ago

Idea: add rule config Debounce trigger guard on leading or trailing edge

debounce for: duration, trigger_on: :start|:end

With leading edge option, trigger rule when no triggers have happened in the past X duration. Discard triggers otherwise.

With trailing edge option, delay triggers until no more triggers were received within the past X duration and run only the last trigger.

ccutrer commented 1 year ago

This seems like a more generalized form of for: <duration> on a single trigger, no? Can you flesh out some concrete examples some more to get a feel for how useful it would actually be?

jimtng commented 1 year ago

This is applicable for example when you have triggers on multiple items and you want to update a calculated item.

myitems = Item1, Item2, Item2, Item4
rule do
  changed *myitems
  run { SummaryItem.update "Total: #{myitems.map(&:state).sum}" }
end

You don't want the rule to run 4 times when all Item1 .. Item4 were updated usually at the same time (e.g. detailed properties of something coming from the same binding). debounce for: 1.second, trigger_on: :end is good for this. Most debouncing needs the :end trigger so it should be the default.

A slightly different scenario: If the changes occur continuously without any rest, the SummaryItem would only be "refreshed" / calculated once per second. This would apply even when triggering on just one item, and also the change duration trigger would not work here - as the value never stops changing. In this scenario either trigger_on: :start or :end works equally. In this scenario, one could use a cron/every :second trigger, but what if the changes only occur in bursts.

As to how useful this would be, that's open for debate, hence this post.

ccutrer commented 1 year ago

I like it. It also reminds me I want to do a calculated_item(SummaryItem) { myitems.map(&:state).sum } terse rule (and automatically detect what items need changed triggers; I've figured out a strategy to do that). I could definitely forward any debounce args for that.

My only reservation is naming of parameters so that they're clear to someone unfamiliar with the code. These are by no means recommendations, I'm just brainstorming:

# ideas for the continuously updating items use case
debounce for: 1.second, max_wait: 1.second
debounce for: 1.second, continuous: true
debounce for: 1.second, wait_for_settle: false
debounce for: 1.second, wait_for_quiesce: false

# trigger_on: :start
debounce for: 5.minutes, on_start: true
debounce for: 5.minutes, leading: true
debounce for: 5.minutes, trigger_on: :leading_edge

# maybe different term for the duration?
debounce over: 5.minutes
debounce period: 5.minutes

If/when you implement this, you should redo the current changed Item, for: duration to use the same debounce back end.

jimtng commented 1 year ago

I've just thought of one application for a leading edge debouncing: Prevent door bell from being pressed repeatedly. We want to trigger on the first press and ignore subsequent presses within X seconds.

ccutrer commented 1 year ago

Omg yes please. I currently already have that exact rule, but implemented by just storing a timestamp in a map of the last time it was triggered, iirc.

I have a "doorbell" on the keypad at each fence gate, and my youngest love to just pound on that button sometimes.

ccutrer commented 1 year ago

See also https://github.com/boc-tothefuture/openhab-jruby/issues/356, which seems to be a subset of the full "debounce" functionality, but is also some ideas for how to name things (though possibly already shot down?).

jimtng commented 1 year ago

Just recording an idea that came to mind. Also implement a DSL.debounce so it can be used within a UI rule:

debounce for 1.minute do
  ...
end
jimtng commented 1 year ago

An idea: instead of using one debounce guard and making it complex, we introduce three guards:

  debounce_for 3.seconds[, max_wait: 10.seconds]
  throttle_for 3.seconds # this is a trailing edge debouncer
  only_every 3.seconds # this is a leading edge debouncer
  1. debounce_for ensures that there's a minimum duration between two triggers before it would let the rule runs. max_wait is an optional argument to limit the overall amount of time that it waits, if triggers never stopped for longer than the interval.
  2. throttle_for ensures that the rule runs no more often than the given duration. It does not require a minimum interval between triggers. When triggers happen frequently, throttle_for simply doesn't run the rules until the given duration had elapsed, regardless of how long ago was the previous trigger.
  3. only_every it's similar to throttle_for except it executes on the leading edge of the trigger.

Whilst not as flexible as the original debounce, these three options seem a lot easier to understand, and it should cover most needs, I would think.

thoughts?

ccutrer commented 1 year ago

I need to understand max_wait and min_idle first. how does min_idle factor into these three options?

jimtng commented 1 year ago

I've changed debounce_for so it uses a range instead of max_wait. It's more intuitive that way I think.

        # @param [Duration,Range] debounce_time The interval between triggers before the rules
        #   are allowed to run. When specified just as a Duration or an endless range, it
        #   will continue to wait until the given duration has elapsed since the last trigger
        #   has passed before executing the rule.
        #
        #   The end of the range when specified, sets the maximum amount of time to wait
        #   before the rule will execute regardless of the interval between triggers.

The first part of that param describes min_idle. Basically it means your events need to be separated apart by at least min_idle. It's the "quiesce" time that needs to occur before execution is allowed. So if triggers are too busy and keeps happening at a rate faster than min_idle, the debouncer will not execute the call / rules.

I will try to explain more about this in the timing diagram too

jimtng commented 1 year ago

When you have events that fire too fast all the time, with just min_idle, you'll never get an execution because min_idle is never satisfied. max_wait overrides this to ensure that executions will happen at least every max_wait interval even though min_idle isn't met.