Accessing variables in lambda

Hi,

is there a way to access a variable defined in a rules file inside an Lambda? I have one “rule global” variable, that is needed in the rule and inside the lambda. But it seems to be not accessible inside the lambda.

Any suggestions?
Pascal

Yep, pass the variable to the lambda as on of its parameters.

Or use items as variables.

I’ll use this as another opportunity to do me lambda speech.

Any time you see [ | ] you are seeing a lambda. So createTimer, forEach, filter, and all the rest of the times you see the square brackets you are defining a lambda.

There are two places you can define a lambda, inside a Rule and outside of a Rule.

When you define a lambda inside a Rule, the lambda will get a copy of the whole context of that Rule. So triggeringItem, receivedCommand, all your globals, and anything else that has been defined up to the point where you created the lambda will also exist inside the lambda. This is why we can do something like:

timer = createTimer(now.plusSeconds(1),  [ | timer = null ])

The lambda created and passed to createTimer has access to the variable timer because timer existed as a variable before the lambda was created.

When you define a lambda as a global, which is what you are really asking about, there is no context to inherit. Consequently a globally defined lambda stands in isolation. So if the lambda needs access to a variable, even if it is a global, you must pass it to the lambda as an argument. Note that lambdas only support up to six arguments.

One thing to realize is that a lambda is an Object. When you create a global lambda you are creating a single lambda Object that all the Rules that call that lambda share. And calling the lambda is not thread safe. So it is possible for two Rules to call a lambda at nearly the same time and end up stomping on each other. Be very careful with lambdas.

Based on my experience, personal and helping people on this forum, I strongly recommend against the use of lambdas except under the most dire circumstances. It will almost always be worth it to refactor or change your approach to avoid the need for lambdas. Consequently I treat them as a code smell. You would have to pass a very high bar to convince me that they are absolutely required in your situation.

And even if you convince me that the lambda is needed, I’m likely to recommend farming out the task to a separate Rule (Design Pattern: Separation of Behaviors) or to an external script or service.

About the only time I’m OK with the use of global variables any more is like the use in OAuth2 using just OH Rules and myopenhab.org where the lambas encapsulate code that must be called from multiple different Rules BUT are almost guaranteed that the lambda will never be called twice at the same time.

3 Likes

@rlkoshak I don’t want to start a discussion about pros and cons of lambdas. You have way more experiences in developing openhab rules so I accept, that your are the expert.
I’m ruby developer and this leads me to two problems: I hate unstructured code and I’m using lambdas in nearly every situation :smiley:

Maybe you have an idea, how to replace the lambdas without blowing up the code. Hint: There will be more groups to iterate through, not only temperature. It’s the first draft.

import java.util.ArrayList

var PERSISTENCE = "mapdb"
var NOT_UPDATED_SINCE_MINUTES = 30
var ArrayList<GenericItem> NOTIFICATION_MEMORY = newArrayList()

// Define an updatedSince methode because buildin function won't do the job
// see: https://community.openhab.org/t/updatedsince-leads-to-wrong-results/2319

val Functions$Function2<GenericItem, Number, Boolean> notUpdatedSince= [ GenericItem s, Number NOT_UPDATED_SINCE_MINUTES |
  var lastUpdate = s.lastUpdate(PERSISTENCE)
  var compareDate = now.minusMinutes(NOT_UPDATED_SINCE_MINUTES)
  var difference = compareDate.millis - lastUpdate.millis
  var notUpdated = true
  if (difference < 0) {
    notUpdated = false
  }
  notUpdated
]

rule "Warning - Missing Sensor Values"
when
  Time cron "0 /30 * * * ?"
then
  gTemperature.members.filter[ i | notUpdatedSince.apply(i, NOT_UPDATED_SINCE_MINUTES)].forEach[ i |
    if (NOTIFICATION_MEMORY.contains(i)) {
      return
    }

    var message = "Temperature Value of Sensor '" + i.name + "' not updated for at least " + NOT_UPDATED_SINCE_MINUTES + " minutes. Last Updated at '" + i.lastUpdate + "'."
    logWarn("Temperature", message)
    pushNotification( "Warning - Missing Sensor Values", message)

    NOTIFICATION_MEMORY.add(i)
  ]
end

rule "Warning - Missing Sensor Values: Clear Notification Memory"
when
	Time cron "0 0 0 * * ?"
then
  NOTIFICATION_MEMORY.clear
  logInfo("Temperature", "Cleared Notification Memory for 'Missing Sensor Values' Rule")
end

THX for your great engagement here.
Pascal

Rules DSL isn’t unstructured, it is differently structured. If you force it to use lambdas and fake data structures using HashMaps and ArrayLists and such you will end up with excessively long and brittle Rules that are hard to maintain and throw inexplicable exceptions periodically.

I have found that those OH users who are also developers are pretty much always much happier when they use JSR223 Rules instead of Rules DSL. This will let you code in Jython, JavaScript, or Groovy which WILL let you structure your Rules in a way more like what you are use to.

If you stick with the Rules DSL, look to the Design Pattern postings for some of the best practices for how to structure your Rules DSL Rules to avoid lambdas and other code smells. A tl;dr is to use Groups, lots of Groups, and then take advantage of Member of triggers, collections operations, and implicit variables like triggeringItem. When you do this you will find that all those Rules that called the same lambda will shrink by 50% in lines of code and will end up merged into one Rule, eliminating the need for lambdas.

OK, so the code above looks like you are trying to get an alert when a sensor doesn’t report after a certain amount of time.

I would (and do) use the Expire binding. Put all the sensors into the same Group (think of Groups more as tags or sets instead of a hierarchy) I’ll call gSensors.

On each Sensor Item use the expire binding. { blah blah blah expire="30m" } When that Item goes 30 minutes without an update it will get updated to UNDEF.

Then the Rule becomes:

import java.util.List

val List<String> notification = createArrayList

rule "Warning - Missing Sensor Values"
when
    Member of gSensors changed to UNDEF
then
    if(notification.contains(triggeringItem)) return;

    // See https://community.openhab.org/t/design-pattern-separation-of-behaviors/15886
    aAlert.sendCommand("'" + transform("MAP", "sensors.map", triggeringItem.name) +"' not updated for at least 30 minutes. Last updated at '" + triggeringItem.previousState(true).getTimestamp + "'.")
    notification.add(triggeringItem.name)    
end

rule "Clear notification indicators"
when
    Time cron "0 0 0 * * ?"
then
    notification.clear
    logInfo("Temperature", "Cleared Notification Memory for 'Missing Sensor Values' Rule")

    val message = new StringBuilder().append("The following sensors are known to be offline: ")
    message.append(gSensors.members.filter[ s | s.state == UNDEF ].map[ name ].reduce[ str, name | str = str + name + ", " ])
    message.delete(message.length-2, message.length)

    aInfo.sendCommand(message.toString)
end

45+ LOC (plus because you would still need to write the Rule to handle your other sensor types to call your lambda) down to 28 LOC with no need to write any more Rules for your other sensor types, just add those sensors to the gSensors Group. I did change the behavior slightly to send an info alert with a digest message telling you all the sensors that are known to be offline.

I usually write my Rule to also trigger and send an alert when a sensor goes from UNDEF to a valid value if I previously send an alert so I can also send a message when it comes back online.

But as you can see, the code is still structured, but it is structured by Rule. I tag those Items that I want to process the same way and trigger a Rule to do that. All the code is kept in a single and self contained Rule so there is no need for a lambda. This also performs a little better because it doesn’t rely on polling every 30 minutes. You will get an alert when the sensor goes offline.

Also, this Rule is a bit more flexible. For example, if you are using MQTT, you can subscribe to a LWT message which will get sent when the sensor goes offline (i.e. fails to maintain a heartbeat with the MQTT broker). You can set the sensor to UNDEF when the LWT message is received which will also trigger this Rule.

1 Like

I like your solution, it’s well structured, even if you couldn’t avoid using lamdas :wink:

I thought of JS223 in the early times, when it was experimental implemented in OH1. But it thorns me back and forth. On the one hand it will bring me to a world that I understand better but on the other it will lead to new questions with fewer people that could help.

I should make clear, only global lambdas are a problem. Any lambda created within a Rule is just fine because it is local to that Rule instance and there will be no shared resources. In fact they are unavoidable when doing collections operations or creating Timers.

One thing to be aware of is that both JSR223 Rules and the Experimental Next-Gen Rules Engine are basically the same thing. So in a way, switching to JSR223 would be forward thinking. but it is true that there is less documentation and fewer users to help right now. But that is quickly changing,

But don’t worry. Before the current Rules DSL engine is retired, your current Rules will be supported so you won’t have to change anything once the “experimental” is removed from the new engine’s name.

@rlkoshak I found your very helpful message (quoted above) concerning the inheritance of context into a lambda function, and I am wondering how “deep” does the inheritance go? I am planning to use the rule below, to send emails when various alarm Items are set. Some of the alarms may be transient (e.g. the hub offline alarms), so I want to “debounce” them with a timer to avoid sending emails in such transient cases.

As you see in the code below, I push the triggeringItem into the lambda and after the timer has expired I check if triggeringItem.state is still in the ON state; and if so I send the email. – My question is whether the triggeringItem.state value is the Item’s state after the timer delay has expired, of if it is the value that was pushed into the lambda when the timer was created?

rule "Send mail: Various Alarms"
when
    Member of g_Battery_Low_Alarm changed or
    Member of g_Hub_Offline_Alarm changed or
    Member of g_Temperature_Alarm changed or
    Member of g_Home_Open_Window_Detected changed
then
    val mailer = getActions("mail","mail:smtp:g24")
    switch (newState) {
        case ON: {
            var timer = createTimer(now.plusMinutes(5), [ |
                if (triggeringItem.state == ON) { // <= is the alarm still on ?
                    mailer.sendMail("me@home.com", "openHAB", triggeringItem.label)
                }
                timer = null
            ])            
        }
...

Answer: its value is the Item’s state after the timer delay has expired

Sorry for the tardy reply, I was up in the mountains the last few days.

As you indicate, the state is the current state of the Item when the timer runs. This is because the Item Object goes to the Item registry to pull the current state of the Item instead of caching the state.

I use code like yours all over the place and it’s a good pattern to use.

For those who are using Jython rules, I’ve written a reusable debounce library that lets you just set some Item metadata on the Item and create a proxy Item to hold the debounced value and that’s it. No need to mess with timers or anything like that. I’ve managed to save about 100 lines of code since writing and adopting that. So your rule above would become:

Items:

Create a “Raw” Item for each member of the above groups and move the link to the Channel to the Raw. Keep your existing Items as the proxies. On the “Raw” Item add debounce metadata like the following example:

Switch RawSensor { channel="blah:blah:blah", debounce="ProxySensor"[timeout="5m"]}
Switch ProxySensor

You can define a different debounce time on an item by item basis. Then you can get rid of the Timer stuff from your rule entirely:

from core.rules import rule
from core.triggers import when

@rule("Send mail: Various Alarms")
@when("Member of g_Battery_Low_Alarm changed")
@when("Member of g_Hub_Offline_Alarm changed")
@when("Member of g_Temperature_Alarm changed")
@when("Member of g_Home_Open_Window_Detected")
def mail_alert(event):
    if event.itemState == ON:
        actions.get("mail", "mail:smtp:g24").sendMail("me@home.com", "openHAB", ir.getItem(event.itemName).label)

The debounce library won’t set the proxy Item to the new state until it has been.

I’m mainly posting this to show a Jython example to get more Jython examples out on the forum. In your case, if you don’t care to have a different debounce time per Item it probably doesn’t make sense to create all those new Items.

Many thanks @rlkoshak, that looks like a cool solution.

Notwithstanding which, I’m going to stick to the timer solution, since I already got it working fine. :slight_smile:

In my sample code above, in the lambda function, I set “timer = null” in order to self destroy the timer object. However the OH rules compiler issues a warning about that null variable assignment never being used. So I am thinking that the “timer = null” can actually be deleted; as the Java garbage collector should anyway be keeping track of the reference count, and cleanup accordingly. Or??

You don’t need to set the timer variable to null inside the lambda. When the rule runs, it creates a timer variable local to the rule. That gets passed to the lambda as part of the context. Then the rule exits and the rule’s context gets destroyed meaning the timer variable only exists inside the lambda. When the lambda is finally run and exits, unless the timer is rescheduled from inside the lambda, the lambda will then get destroyed too. Because the timer variable only exists inside the lambda, at that point the timer variable will be destroyed and the timer object marked as available to be garbage collected.

1 Like

^
Thank you @rlkoshak