Design Pattern: Rate Limit

Edit: Updates for OH 4.

Please see Design Pattern: What is a Design Pattern and How Do I Use Them first.

Problem Statement

Often one will be in a situation where an event will occur many times but an action should only be taken on the event once over a certain time period and the rest ignored. This is related to Gatekeeper but differs in that Gatekeeper queues up the actions and works them off in time whereas Event Limit throws away events that occur too soon after the last processed events.

This is best explained with an example.

For example, in my own rules I generate an alert when the temperature outside is a few degrees cooler than the inside (in the summer) to open the windows and another alert when it is warmer outside than inside to close the blinds.

Every time the temperature changes both inside and outside there is the potential to generate this alert. Given that some of the thermometers report every two minutes receiving an alert every time is unacceptable.

Concept

Create a flag of some sort which gets set when the first event occurs. Each subsequent event will check this flag and suppress the activity if the flag indicates it’s too soon. The simplest way to implement this is to use a timestamp as the “flag” and test to see if enough time has passed since that timestamp before allowing a new event to be processed.

Blockly

The openHAB Rules Tools [4.1.0.0;4.9.9.9] Block Library provides a rate limit block that implements this design pattern.

image

To implement this yourself it would be something like the following.

JS Scripting

The OHRT library used above in the Blockly example is also available in JS Scripting.

var { RateLimit } = require('openhab_rules_toolks');

var rateLimit = cache.private.get('RateLimitName', () => RateLimit());
rateLimit.run(() => console.info('Rate limited action'), 'PT24H');

Rules DSL

val lastAction = privateCache.get('LastAction', [ | now ]);
if(lastAction.isBefore(now.minusHours(24)) {
  logInfo('RateLimit', 'Rate limited action');
  privateCache.put('LastAction', now);
}
7 Likes

JRuby

This design pattern is also supported in the built-in JRuby helper library using only_every

only_every 24.hours do
  logger.info "Rate limited action"
end

My favourite use of this pattern is for my doorbell, to prevent people from ringing it in rapid succession.

only_every(1.minute) { Audio.play_sound("doorbell.mp3") }

only_every is also supported in JRuby file-based rules as a rule guard

3 Likes

HABApp

This design pattern is also supported by HABApp through the built in rate limiter.

from HABApp.util import RateLimiter

# Create or get existing rate limiter, name is case insensitive.
# The same name can be used from multiple files to get the same limiter
limiter = RateLimiter('MyRateLimiterName')

# Define limits, duplicate limits of the same algorithm will only be added once
# These lines all define the same limit so it'll result in only one added limit
limiter.add_limit(5, 60)   # add limits explicitly
limiter.parse_limits('5 per minute').parse_limits('5 in 60s', '5/60seconds')  # add limits through text

# add additional limit while explicitly specifing leaky bucket algorithm
limiter.parse_limits('10 per hour', algorithm='leaky_bucket')

# Test the limit without increasing the hits
for _ in range(100):
    assert limiter.test_allow()

# the limiter will allow 5 calls ...
for _ in range(5):
    assert limiter.allow()

# and reject the 6th
assert not limiter.allow()
3 Likes

Running OH 4.1.1 on RPi4

Installed the openHAB Rules Tools block Library to use the rate limit function.
Now I’m getting an error that indicates the tools cannot load. Is there some dependency I’m missing?

07:15:27.334 [ERROR] [ab.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: TypeError: Cannot load CommonJS module: 'openhab_rules_tools'
        at com.oracle.truffle.polyglot.PolyglotMapAndFunction.apply(PolyglotMapAndFunction.java:46) ~[?:?]
        at org.openhab.automation.jsscripting.internal.OpenhabGraalJSScriptEngine.lambda$13(OpenhabGraalJSScriptEngine.j                        ava:266) ~[?:?]
        at java.util.Optional.orElseGet(Optional.java:364) ~[?:?]
        at org.openhab.automation.jsscripting.internal.OpenhabGraalJSScriptEngine.lambda$11(OpenhabGraalJSScriptEngine.j                        ava:266) ~[?:?]
        at <js>.:=>(<eval>:12) ~[?:?]
        at <js>.:program(<eval>:11) ~[?:?]
        at org.graalvm.polyglot.Context.eval(Context.java:399) ~[?:?]
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458) ~[?:?]
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426) ~[?:?]
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262) ~[java.scripting:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocableAndAutocloseable.                        eval(DelegatingScriptEngineWithInvocableAndAutocloseable.java:53) ~[?:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAu                        toCloseable.eval(InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.java:78) ~[?:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocableAndAutocloseable.                        eval(DelegatingScriptEngineWithInvocableAndAutocloseable.java:53) ~[?:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAu                        toCloseable.eval(InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.java:78) ~[?:?]
        at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.lambda$0(ScriptActionHandler.j                        ava:71) ~[?:?]
        at java.util.Optional.ifPresent(Optional.java:178) [?:?]
        at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.execute(ScriptActionHandler.ja                        va:68) [bundleFile:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1188) [bundleFile:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.runRule(RuleEngineImpl.java:997) [bundleFile:?]
        at org.openhab.core.automation.internal.TriggerHandlerCallbackImpl$TriggerData.run(TriggerHandlerCallbackImpl.ja                        va:87) [bundleFile:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) [?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304                        ) [?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) [?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) [?:?]
        at java.lang.Thread.run(Thread.java:840) [?:?]
07:15:27.352 [INFO ] [openhab.event.ItemStateUpdatedEvent  ] - Item 'WeatherStation_HumidityOutside' updated to 86 %
07:15:27.361 [INFO ] [openhab.event.ItemStateUpdatedEvent  ] - Item 'WeatherStation_WindDirection' updated to NNW
07:15:27.363 [INFO ] [openhab.event.ItemStateChangedEvent  ] - Item 'WeatherStation_WindDirection' changed from N to NNW
07:15:27.365 [INFO ] [openhab.event.ItemStateUpdatedEvent  ] - Item 'WeatherStation_WindSpeed' updated to 1.44 km/h
07:15:27.374 [INFO ] [openhab.event.ItemStateChangedEvent  ] - Item 'WeatherStation_WindSpeed' changed from 1.8 km/h to                         1.44 km/h
07:15:27.376 [INFO ] [openhab.event.ItemStateUpdatedEvent  ] - Item 'WeatherStation_GustSpeed' updated to 1.8 km/h
07:15:27.378 [INFO ] [openhab.event.ItemStateUpdatedEvent  ] - Item 'Weather_Station_Light_Intensity' updated to 9063 lx
07:15:27.380 [INFO ] [openhab.event.ItemStateChangedEvent  ] - Item 'Weather_Station_Light_Intensity' changed from 9403                         lx to 9063 lx
07:15:27.383 [INFO ] [openhab.event.ItemStateUpdatedEvent  ] - Item 'WeatherStation_TemperatureChannel1' updated to 21.2                         °C
07:15:27.385 [ERROR] [.internal.handler.ScriptActionHandler] - Script execution of rule with UID 'TempLower' failed: org                        .graalvm.polyglot.PolyglotException: TypeError: Cannot load CommonJS module: 'openhab_rules_tools'

Be sure to read the docs for the Block library as well. This design patter shows one way to use the blocks but it does not reproduce the full docs for the Block library.

This library requires openhab_rules_tools to be installed. This can be installed from openhabian-config or by running the command npm install openhab_rules_tools from the $OH_CONF/automation/js folder.

Further information about installation can be found at GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules. which is also linked to from the Block library docs.

Not a fan of Openhabian

I assumed all I had to do was install OpenHAB Rules tools from the marketplace. After I did that the additional tools showed up in the Blockly workspace.

I guess there’s more to it. I will need to study.

pi@jensen:/etc/openhab/automation/js $ npm install openhab_rules_tools
-bash: npm: command not found

obviously I’m missing something

obviously the npm binary which is part of the npm package.

You should always read the docs for the add-on, widget, rule template, or block library that you install from the marketplace.

Blockly is a graphical programming environment. The blocks in Blockly convert to JavaScript, JS Scripting to be precise. JS Scripting provides a Node.js like JavaScript programming environment.

OHRT is a node.js library written for openHAB. The OHRT Block Library is simple some blocks that expose the capabilities of the OHRT library as Blocks.

npm is the package manager for Node.js. It can be used to install node libraries like OHRT.

If you don’t have npm installed on your host, you need to install it, or you need to manually install the library as described above.