Design Pattern: Gate Keeper

Edit: Updated for OH 4, removed Nashorn JS and Python examples as those libraries are no longer maintained.

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

image

Problem Statement

There are some technologies that are sensitive to receiving multiple commands too close together. When they occur too close together commands get dropped or other problems may occur. Insteon and 433MHz are two such technologies with this problem.

There are other situations where you may want to schedule a series of events to occur with a defined spacing between them. For example, to schedule the amount of time to irrigate a number of zones. This also applies to scheduling delays between calls to the say command or scheduling the startup of a home entertainment system (e.g. turn on the stereo, wait five seconds, turn on the TV, etc.).

Concept

Centralize all the communication with these technologies into a single gatekeeper and add delays between each command sent to the devices so no two commands, regardless of their target device, ever get sent too close together or have the desired spacing between commands.

Example

We can take advantage of Design Pattern: Associated Items and Design Pattern: Encoding and Accessing Values in Rules to make a generic solution.

Note: The code below depends on the the WirelessDevice Items to be persisted.

Items

Group WirelessDevices
Switch WirelessDevice_xxxxx_1 (WirelessDevices)
Switch WirelessDevice_xxxxx_1_Proxy // linked to channel
Switch WirelessDevice_xxxxx_2 (WirelessDevices)
Switch WirelessDevice_xxxxx_2_Proxy // linked to channel

Rules

Create a rule triggered by “Member of WirelessDevices” and ensure that commands to the actual devices are at least one second apart.

Blockly

The OHRT Block Library includes a block that implements a gatekeeper.

JS Scripting

This implementation depends on openHAB Rules Tools Announcements

var {Gatekeeper} = require('openhab_rules_tools');
var gk = cache.private.get('gk', () => Gatekeeper());
gk.addCommand(() => items[event.itemName+'_Proxy'].sendCommand('ON'));

Rules DSL

import java.util.concurrent.ConcurrentLinkedQueue
import java.time.Duration

val Queue<Sring> items = new ConcurrentLinkedQueue()
val Queue<String> commands = new ConcurrentLinkedQueue()
var Timer timer = null
var lastCommand = now.minusSeconds(1).millis

rule "WirelessDevices gatekeeper"
when
    Member of WirelessDevices received command
then
    items.add(triggeringItemName)
    commands.add(receivedCommand.toString)

    if(timer === null) {
        timer = createTimer(now, [ |
            if(commands.peek !== null) {
                val item = items.poll
                val cmd = commands.poll
                sendCommand(item+"_Proxy", cmd)
                lastCommand = now
            }

            val deltaTime = Duration.between(now, lastCommand).toMillis();
            timer.reschedule(now.plusMillis(if(deltaTime<1000) 1000-deltaTime else 0) // 0 will reschedule the timer to run immediately
        ])
    }
end

Theory of Operation:
When a command comes in to a WirelessDevice Item, the name of the Item and the command is queued up. A looping timer is created which sends the command and waits the desired amount of time before sending the next command until the queue is empty. If the timer is running, the subsequent commands will be queued and worked off in turn.

Advantages and Disadvantages

Advantages

  • Does not tie up a Rule runtime thread sleeping

  • Can be used to schedule a series of events (particular the Python version), such as turning on a stereo receiver and waiting a couple seconds before starting the audio stream to it, giving it time to start up before playing the audio. The following is a Blockly example for an irrigation schedule.

  • Super easy to implement and use with the block library or OHRT library.

Disadvantages

  • Requires creating a separate set of Rules for each Gatekeeper required (e.g. one for TTS say commands and other for Hue bulbs).
  • The Rules DSL version has a hard coded delay between commands. The others let you specify a different delay after each command.

Related Design Patterns

Design Pattern How It’s Used
Design Pattern: Separation of Behaviors Gate Keeper is a specific implemenation of Separation of Behaviors
Design Pattern: Proxy Item The WirelessController and Device Items
Design Pattern: Associated Items Mentioned in the description but not used above
Design Pattern: Encoding and Accessing Values in Rules The naming for the device Items
Design Pattern: Looping Timers The implementation of the loop that works off the queue in the Rules DSL version
14 Likes

Hadn’t seen that approach to locking rules before - very neat (I’d always used a variable to achieve that indirectly).

Hi Rich.
Want to use it for my hue lights.

Group:Switch:OR(ON, OFF) all_lights  "Wohnzimmer_Sofa"
Switch Licht_EG_Sofa1_Switch         "Licht Sofa1_Switch"     (all_lights)         {channel="hue:0210::8:color"}
Switch Licht_EG_Sofa2_Switch         "Licht Sofa2_Switch"     (all_lights)         {channel="hue:0210::4:color"}
Switch Licht_EG_Sofa3_Switch         "Licht Sofa3_Switch"     (all_lights)         {channel="hue:0210::5:color"}

Can you give me an example how i can use your rule?
Thank you very much.
Greetings;
Markus

The complex example is all that you need. If you have specific problems I’d love to help but I’m not inclined to write someone else’s rules if they haven’t even attempted writing their own.

Yes, of course. I understand.
The problem is that i don’t understand the thing with device ID and controller ID.

2018-07-16 02:16:27.150 [WARN ] [lipse.smarthome.io.net.exec.ExecUtil] - Execution failed (Exit value: -559038737. Caused by java.io.IOException: Cannot run program "zigbee-send" (in directory "."): error=2, No such file or directory)
import java.util.concurrent.locks.ReentrantLock
var lock = new ReentrantLock
var lastCommand = now.millis
val commandDelay = 100 // experiment to find the lowest number that works

rule "A WirelessDevice received a command"
when
    // We can't trigger the rule using WirelessDevices received update because there is no good way to handle the multiple rule triggers
    Item Licht_EG_Sofa1_Switch  received command or
    Item Licht_EG_Sofa2_Switch  received command or
    Item Licht_EG_Sofa3_Switch  received command
then

    // Get the controller ID and device ID
    val split = triggeringItem.name.split("_")
    val controller = split.get(1)
    val device = split.get(2)
    val command = if(receivedCommand == ON) "1" else "0"

    // Ensures only one instance of the Rule can run at a time
    // We do this after the lines above so the delay below does not interfear with the Rule's ability to 
    // determine which Item triggered the Rule
    lock.lock
    try {
    
        // Sleep if the last command happened too close to now, but only sleep just long enough
        val deltaTime = now.millis - lastCommand // how long since the last call to executeCommandLine
        if(deltaTime <= commandDelay) Thread::sleep(deltaTime)

        val results = executeCommandLine("zigbee-send " + controller + " " + device + " " + command, 5000)
        lastCommand = now.millis

        logDebug("zigbee", results)
    }
    catch(Exception e) {
        logError("zigbee", "Error handling zigbee command: " + e)
    }
    finally {
        lock.unlock
    }
    
end

That warning is coming from the program you are running (zigbee-send) not the rule. You probably have to provide the full path to the command.

Look at How to solve Exec binding problems

Oh, now i understand the “executeCommandLine” part. :joy:
Thanks and greetings,
Markus

Shouldn’t be there:
sleep(commandDelay - deltaTime) ?

If the deltaTime since lastCommand is only 20 ms, then we wait only another 20 ms, which is 40 ms total and this does not help us to reach the desired 100 ms delay.

That is correct. We need to wait the full commandDelay time at a minimum.

Yeah, first post here! Just wondering, I already had such a pattern with a queue in mind as I experience pretty much the same problem with my 433Mhz RFLink bridge.
However, I would prefer this kind of message handling would be realized correctly inside the bindings. Eventually these are the controller of the devices and have to take care of the message flow, not the rules. Why can’t we just state such a behavioral requirement for the bindings, specifically those with such problematic protocols? The delay could be exposed as a configurable parameter then. (I plan to try this way with a fork of the RFLink binding).

All I can say is it’s because the binding developers didn’t implement it that way.

Well, that’s unfortunate. However, when it comes to the RFLink binding there seem to exist some tiny delay within the serial communication. But it’s not configurable. Maybe worth to expose it to skip the hassle inside the rules…

Hello all,
I hope a comment on this pattern is a good place to put my question - if it is not, please let me know. I will open up a new thread then.

My goal

I want to send out notifications via telegram from rules

My approach

  • proxy string TNotificationViaTelegram for the message to be sent
  • any rule may set it, e.g. TNotificationViaTelegram.sendCommand(tMsg)
  • a central rule which reacts to changes in this item:
rule "Send Notification over Telegram"
  //notify via Telegram
  when
    Item TNotificationViaTelegram received update
  then
    if (SNotificationViaTelegram.state != ON) {
      return;
    }

    val String msg = TNotificationViaTelegram.state.toString
    
    sendTelegram("HomeInfo", msg)
  end

Problem

In the Telegram message stream, I sometimes miss a message and instead get the same message twice in quick succession.
I am pretty certain, that this is what is going wrong:

  1. rule A updates TNotificationViaTelegram
  2. rule B updates TNotificationViaTelegram (in quick succession - this e.g. happens by rules which react to presence)
  3. rule Send Notification over Telegram gets triggered by the rule A update, but the actual sending happens, rule B has already changed the content - ergo the rule B message gets sent
  4. rule Send Notification over Telegram gets triggered by the rule B update, the rule B message gets sent

Solution approaches

Note: these are the ones I can think of, I have not tried out any of them yet - still theorizing :wink:

  1. Add a Thread::sleep after every instance of TNotificationViaTelegram.sendCommand(tMsg)

    • Unsexy approach, as it is not centralized and I have to think of it whenever I sent a message
    • I don’t think it will work 100%, as when a certain event (presence) triggers two rules, these two rules might still update TNotificationViaTelegram at nearly the same point of time. This would be less likely to happen, if I use randomized sleep-times, which makes working with this approach even more complicated.
  2. Approach as suggested in the “Simple Example”

    • If I understand correctly, this would not solve my problem (or would it??).
      I believe the approach makes a rule a singleton. But my problem is not the rule running in parallel, but that TNotificationViaTelegram is changed while the rule is running.
  3. Approach as suggested in the “Complex Example using Queues”

    • In essence, a FIFO queue is what would solve my problem. The way the example is set up though, the queue does not span across mutiple rules-files - correct?

So what is your view on this? Can anyone give me some pointers how to get this done?
Maybe there is a simple mechanism which I am use but don’t know or fail to recognize?

By the way: @rlkoshak - I have been using OpenHab for ca. 2 years now and your posts in this forum have been tremendously helpful for my understanding. I am amazed how often you find the time to take part in a discussion and help out with your knowledge and views.
Thank you very much for that!

This is probably not a Gate Keeper problem, though it might be solvable with it, though I don’t think that’s necessary.

Use commands instead of updates to trigger your Rule. Command makes sense here as you are activating an external service, not just updating OH’s internal state. Then, when you use received command, you can use the receivedCommand implicit variable which will always be what was commanded regardless of timing.

Option won’t do anything because that Thread::sleep won’t affect the other Rules that call the sendCommand. It will just delay when that one instance of the Rule will exit.

Option two and three won’t really solve your problem because your actual problem, if I understand correctly, is that when two updates occur too close together one of them get’s overwritten before it can be processed by the Rule. Using a lock or a timer or the queue will only add delays between calls to telegram so you don’t bombard the service with too many calls all at the same time. But the data problem you are seeing would still be a problem.

You might still want to use a Gate Keeper to spread out the messages sent to Telegram, but you need to use commands in order to keep the states from overwriting each other.

You are most welcome! I learn by helping and contributing to the forum is my way of giving back to the community.

1 Like

@rlkoshak: so you offer real-time problem solving services - cool :wink: ! In my job, I have access to some paid “premium support services” which are nowhere as good (or quick) as you are :smiley:

Thanks - I fully understand what you say. I even remember reading about the receivedCommand variable at some point. But I never used it and forgot about it.
I’ll go that way - I am very confident it’ll work!

You just happened to catch me when I was online. Though when I’m tagged I get an email and can often reply from my phone, which was the case here.

Edit: @5iver solved my problem and I have corrected the code so that one can copy&paste items and code if one have a similar use case. @rlkoshak thank you for this great design pattern!

Hi @rlkoshak , this is the case with my sonos speakers, when there are multiple notifications to be played.
That’s why I try to use the complex example of your design pattern to manage playing notifications.
But there are errors in my openhab.log that I cannot solve and I couldn’t find a solution in the forum.

My items in file GateKeeperExamples.items

Group GrpMessages
Switch CreateMessage    "CreateMessage [%s]"    (GrpMessages)
String TextQueued       "TextQueued [%s]"       (GrpMessages)
String TextOutput       "TextOutput [%s]"       (GrpMessages)

My code in file GateKeeperExamples.rules

// IMPORTS **************************************************************************  
    import java.util.concurrent.ConcurrentLinkedQueue

// GLOBAL VARIABLES ************************************************************
    val MyQueue = new ConcurrentLinkedQueue()    // Edited this line. The code causing the errror was: val Queue<String> MyQueue = new ConcurrentLinkedQueue()
    var Timer timer = null
    var lastCommand = now.minusSeconds(1).millis

    var MyMessageCounter = 0

// RULES ****************************************************************************

rule "rGateKeeperExample_CreateMessage"
 when
    Item CreateMessage received command
 then
    MyMessageCounter = MyMessageCounter + 1     // MyMessageCounter see global variables section
    val MyMessage = "Message no. " + MyMessageCounter.toString + " , date = " + now.toString
    TextQueued.sendCommand( MyMessage )
    logInfo("openhab", "New Message = " + MyMessage )
end // EndOfRule rGateKeeperExample_CreateMessage

rule "rGateKeeperExample_MessageOutput"
 when
    Item TextQueued received command
 then
    val String          MyLogFile = "MySeparateLogFile"   // if 'MySeparateLogFile' does not exist, logging goes into openhab.log (to create a separate logfile see https://www.openhab.org/docs/administration/logging.html#logging-into-separate-file)
    val DateTime        MyLogDate = now  
    val StringBuilder   MyLogMessage = new StringBuilder
    MyLogMessage.append( "\n" + "\n" + " ----------   rGateKeeperExample_MessageOutput v001   ----------" + "\n" + "\n" )
    MyLogMessage.append( "    MyLogDate = " + MyLogDate.toString + "\n" + "\n" ) // ("yyyy-MM-dd'T'HH:mm:ss.SSSZ")

    MyQueue.add(receivedCommand.toString)  // https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html#add(E)
    MyLogMessage.append( "    TextQueued = " + receivedCommand.toString + "\n" ) 

    if(timer === null) {
        timer = createTimer(now, [ |
            if(MyQueue.peek !== null) {    // https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html#peek()
                val TmpText = MyQueue.poll     // https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html#poll()
                TextOutput.sendCommand( TmpText.toString )
                // say(TextOutput, "voicerss:deDE", "sonos:PLAY1:SonosLiving", new PercentType(25))
                MyLogMessage.append( "       TextOutput = " + TmpText.toString + "\n" ) 
                lastCommand = now.millis
            }
            timer.reschedule(now.plusMillis(60000))  // output of next message after 60s
        ] )
    } 
    logInfo( MyLogFile, MyLogMessage.toString )
end // EndOfRule rGateKeeperExample_MessageOutput

My openhab.log

2019-03-28 20:58:18.129 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'GateKeeperExample.rules'
2019-03-28 21:25:55.014 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'GateKeeperExample.rules', using it anyway:
The field Tmp_GateKeeperExampleRules.MyQueue refers to the missing type Object
The field Tmp_GateKeeperExampleRules.MyQueue refers to the missing type Object
The field Tmp_GateKeeperExampleRules.MyQueue refers to the missing type Object
2019-03-28 21:25:55.209 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'GateKeeperExample.rules'
2019-03-28 21:26:26.329 [INFO ] [lipse.smarthome.model.script.openhab] - New Message = Message no. 1 , date = 2019-03-28T21:26:26.320+01:00
2019-03-28 21:26:26.359 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'rGateKeeperExample_MessageOutput': 'add' is not a member of 'Object'; line 38, column 5, length 37
2019-03-28 21:26:28.888 [INFO ] [lipse.smarthome.model.script.openhab] - New Message = Message no. 2 , date = 2019-03-28T21:26:28.875+01:00
2019-03-28 21:26:28.915 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'rGateKeeperExample_MessageOutput': 'add' is not a member of 'Object'; line 38, column 5, length 37
2019-03-28 21:26:29.725 [INFO ] [lipse.smarthome.model.script.openhab] - New Message = Message no. 3 , date = 2019-03-28T21:26:29.719+01:00
2019-03-28 21:26:29.757 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'rGateKeeperExample_MessageOutput': 'add' is not a member of 'Object'; line 38, column 5, length 37

Problem
I think the info in openhab.log
The field Tmp_GateKeeperExampleRules.MyQueue refers to the missing type Object
indicates, that there is something wrong with the declaration of MyQueue in this line of my code
val Queue<String> MyQueue = new ConcurrentLinkedQueue()

Every time the rule is triggered I get this error
Rule 'rGateKeeperExample_MessageOutput': 'add' is not a member of 'Object'; line 38, column 5, length 37
which points to line 38 with this code MyQueue.add(receivedCommand.toString)

I couldn’t find a solution yet, so every help is appreciated.

My system is openhabian installed on raspberry pi:

  • Release = Raspbian GNU/Linux 9 (stretch)*
  • Kernel = Linux 4.14.79-v7+*
  • Platform = Raspberry Pi 3 Model B Rev 1.2*
  • openHAB 2.4.0-1 (Release Build)*
1 Like

In DSL rules, only be as specific as you need to be.You shouldn’t get the error if you use this…

val MyQueue = new ConcurrentLinkedQueue()
1 Like

Many thanks, that is the solution.
Small cause, big effect!

If I have a rule using a GateKeeper (the complex example using queues), then one timer runs forever:
If a timer runs forever, does it mean, one thread is used forever?

If I have five rules, each using one GateKeeper:
Does the Rules DLS run out of threads?