Working with Xtend Pairs

Hello guys!

I have been trying to use a Pair<A,B> in one of my rules, but I’m getting a Null error when I try to access to the value of the Pair after I inserted it into a Map.

The following rule is a simplification of the real one.

import java.util.Map

var Map<Integer, Pair<Timer, String>> timers = newHashMap
var index = 0

rule "Demo Rule"
when
    Item BoilerAlarm received update
then
    index = index + 1
    val msg = triggeringItem.state 
    val timer = createTimer(now.plusMinutes(1), [| logInfo("demo4", "timeout")]) 
    val item = timer -> msg //pair operator
    //  val item = new Pair<Timer, String>(timer, msg) 

    logInfo("demo4", "Pair key:  " +item.getKey())
    logInfo("demo4", "Pair value:  " +item.getValue())

    timers.put(index, item)

    timers.forEach[k,pair,i | 
        logInfo("demo4", "Key:  " +pair.getKey())  // OK
        logInfo("demo4", "Value:  " +pair.getValue())  // Error, null
     ] 
end

This is the log that I am obtaining

(1) 15:10:52.738 [INFO ] [.eclipse.smarthome.model.script.demo4] - Pair key:  org.eclipse.smarthome.model.script.internal.actions.TimerImpl@30a9d372
(2) 15:10:52.739 [INFO ] [.eclipse.smarthome.model.script.demo4] - Pair value:  IS_FAILURE
(3) 15:10:52.740 [INFO ] [.eclipse.smarthome.model.script.demo4] - Key:  org.eclipse.smarthome.model.script.internal.actions.TimerImpl@30a9d372
(4) 15:10:52.741 [ERROR] [untime.internal.engine.RuleEngineImpl] - Rule 'Demo Rule': Could not invoke method: org.eclipse.xtext.xbase.lib.StringExtensions.operator_plus(java.lang.String,java.lang.String) on instance: null

As you can see in the log (2) shows I access to the value without problems, but in log (4) I get an error.

Any suggestions of what I’m doing wrong?

Thanks in advance,
Humberto

Rules DSL is loosely typed. If I read correct, you store an item.state as value. Maybe store item.state.toString

1 Like

@rossko57 You were right, I did your suggestion and everything runs perfectly :grinning:
thanks again :+1:

It would probably work the other way too - store what you like and do the .toString in the logInfo

@rossko57

I tried the other way and it didn’t work

val msg = triggeringItem.state
...
timers.forEach[k,pair,i | 
        logInfo("demo4", "Key:  " +pair.getKey())  // OK
        logInfo("demo4", "Value:  " +pair.getValue().toString())  
     ]

Output

16:13:29.100 [ERROR] [untime.internal.engine.RuleEngineImpl] - Rule ‘Demo Rule’: Could not invoke method: java.lang.String.toString() on instance: IS_FAILURE

But, with your first suggestion worked :+1:

You probably have to import Pair as well. I just did some work with Maps and Lists and the like and discovered that while they kind of work when you don’t import them, they don’t completely work. And since Pair comes from the javafx package, I wouldn’t be surprised if it is not available for use in OH. I don’t remember if javafx is generally available in Java 8 or not.

I also don’t understand why you need the Map and the Pair. What specific problem are you trying to solve here? Setting up a poor man’s data structure like this in Rules DSL is a code smell. There is almost always a better way to achieve the same thing.

Anyway, the specific error here is that the Rules DSL can’t figure out how to convert the result from calling pair.getValue to a String (null errors like that are almost always a type problem). You can try explicitly calling toString on the value.

Hi @rlkoshak

I was importing the Pair class from javafx, but apparently there is no need for that. When I inspect the item it says that it is a pair:

timers.forEach[k,pair,i | 
        logInfo("demo4", "Type:  " +pair.getClass()) 
        logInfo("demo4", "Key:  " +pair.getKey())  
        logInfo("demo4", "Value:  " +pair.getValue()) 
     ] 

Output

23:20:03.353 [INFO ] [.eclipse.smarthome.model.script.demo4] - Type:  class org.eclipse.xtext.xbase.lib.Pair
23:20:03.354 [INFO ] [.eclipse.smarthome.model.script.demo4] - Key:  org.eclipse.smarthome.model.script.internal.actions.TimerImpl@5154e1f8
23:20:03.356 [INFO ] [.eclipse.smarthome.model.script.demo4] - Value:  FHS_FAILURE

Why a Pair? I need to store each alarm message with its timer, so I can later check the type of message and cancel its timer if it’s needed. That’s why I went for a tuple.
Why a Map? I use an incremental integer as Key to be able to iterate over the map in the same order in which the alarm-msgs were received.

I’m trying to define this rule:

Notify me when the water boiler reports three floor heating sensor (FHS) failures and one internal sensor (IS) failure in one hour.

So, I want to be notified only if the four events occur in a time window of an hour.

Maybe you can suggest another way to go, I have been working with OpenHAB less than a week. Suggestions are welcome! :wink:

Once again, thanks for your comments and your time :+1:

The same occurs with Map, but, for example, if you do not import Map then the Rules DSL will complain there is no getKeys() method. Import it and it works.

It depends on whether you are a programmer or not. If you know how to program then you might be happier in one of the JSR223 languages. Rules DSL is nice and simple and relatively easy to learn for non-coders, but coders, especially those who insist on bending the language to their will rather than adopting to the idiosyncrasies of the language quickly become very frustrated.

OK, so you don’t actually care about the order of the events, just whether they’ve occurred in the last hour. So I would probably do something like the following.

  • Create a Map<String, Integer> to store a count of each of the events that have occurred in the past hour
  • When an event occurs, increment the Integer at that event’s key in the Map
  • Set a Timer for 60 minutes, at which point it decrements the Integer at that key
  • Check the two keys, if the one event has a 3 or more and the other a 1 or more generate your alert.
val Map<String, Number> events = newHashMap("FHS" -> 0, "IS" -> 0) // initialize the Map with zeros so we don't have to check for null in the Rule

rule "Demo Rule"
when
    Item BoilerAlarm received update
then
    // Increment the count
    val eventStr = BoilerAlarm.state.toString // put this into a constant so we can access it in the Timer later on
    val currCount = events.get(eventStr)
    events.put(eventStr, currCount+1)

    // Create the Timer to decrement in an hour
    createTimer(now.plusHours(1), [ |  // We don't ever need to cancel the Timer so we don't need to keep a handle on it
        val count = events.get(eventStr)
        events.put(eventStr, count-1)
    ])

    // Figure out if we need to alert
    if(events.get("FHS") >= 3 && events.get("IS") >= 1) {
        // publish alert, see https://community.openhab.org/t/design-pattern-separation-of-behaviors/15886
        // do any rate limiting if you need to avoid repeated alerts, see https://community.openhab.org/t/design-pattern-event-limit/17831
    }
end

I just typed in the above. There are likely errors.

Hi @rlkoshak

Many thanks for sharing your experience :+1:

Your approach is simpler and cleaner than mine, thanks :+1:

I just finished the setup of the OpenHab Helper Libraries :slightly_smiling_face:
Based on the documentation and examples I will try the Jython :snake: approach.

Have a good day!