Reusable Functions: A simple lambda example with copious notes

No, lambdas have to be supported or else you couldn’t create a Timer nor run a forEach or the like. Those require a lambda as an argument.

I’ve seen no reports of lambdas not working on the forum except this one.

No. But you might find a reason why your rule no longer works. Please post the rule.

Here is the rule in question:

val String fileName = "alert.rules"

val telegramBot = getActions("telegram", "telegram:bot:myOpenhabianBot")

val checkAndSendAlert = [ NumberItem temperature, NumberItem minimum, NumberItem maximum, SwitchItem normal |

    logInfo(fileName, "Checking temperature")

    var sensor = transform("MAP", "alert.map", temperature.name)

    logInfo(fileName, sensor)

    if ((temperature.state as Number) < (minimum.state as Number))

    {

        logInfo(fileName, "Teperature is below minimum")

        if (normal.state == ON)

        {

            sendCommand(normal, OFF)

            telegramBot.sendTelegram("⚡ %s temperature at %.1f°C\nSensor is *below* minimum of %.1f°C",

                sensor,

                (temperature.state as DecimalType).floatValue(),

                (minimum.state as DecimalType).floatValue())

        }

    }

    else if ((temperature.state as Number) > (maximum.state as Number))

    {

        logInfo(fileName, "Teperature is above maximum")

        if (normal.state == ON)

        {

            sendCommand(normal, OFF)

            telegramBot.sendTelegram("⚡ %s temperature at %.1f°C\nSensor is *above* maximum of %.1f°C",

                sensor,

                (temperature.state as DecimalType).floatValue(),

                (maximum.state as DecimalType).floatValue())

        }

    }

    else if (normal.state == OFF)

    {

        logInfo(fileName, "Teperature is normal")

        sendCommand(normal, ON)

        telegramBot.sendTelegram("⚡ %s temperature at %.1f°C\nSensor is back to *normal*",

            sensor,

            (temperature.state as DecimalType).floatValue())

    }

]

rule "Send alert when VrRoom temperature goes out of limits"

when

    Item VrRoom_Temperature changed or

    Item VrRoom_Temperature_Min changed or

    Item VrRoom_Temperature_Max changed

then

    logInfo(fileName, "Checking")

    checkAndSendAlert.apply(VrRoom_Temperature, VrRoom_Temperature_Min, VrRoom_Temperature_Max, VrRoom_Temperature_Normal)

end

A couple things I notice.

  1. There is no reason to put this into a lambda unless you have multiple rules that call it that you’ve not posted.

  2. This lambda should have never worked in OH 2.5 either. checkAndSendAlert is a global lambda. Global lambdas have no context. Consequently it cannot see other global variables unless you pass them to it as an argument. You are attempting to use fileName and telegramBot inside the lambda without passing them as arguments.

  3. I’ve seen many many reports where trying to get the action as store it in a global variable does not work. You should move the call to getActions into where ever the action is called.

  1. You are correct in that I did not post all rules that use the function. There are many and I would love to parameterize it even further to send alerts for humidity as well as temperature.
  2. This is not 100% the version that was running on OH 2.5 since I’ve been trying to get it to work for some time now. Good to know though that this is not allowed. Is there a page with detailed documentation about scripting rules?
  3. That’s something I will try.

See Design Pattern: Associated Items

Rules | openHAB. Be sure to follow the links as well.

In general, a global lambda like this is a real code smell. There are a few cases where it can make sense but this isn’t one of them I think. Lambdas are not thread safe so if you do have more than one rule call it at the same time, the second rule will overwrite the variables from the first one while the first one is still running. And most of the time there is a better way.

Comment only - I think that’s mostly about startup timing, as the xxx.rules file is loaded an attempt is made to get the Action before the binding has prepared it. A circumvention is to get the Action later, when you actually run a rule. I don’t think there’s any problem with the principle of getting a “global” Action, just the startup timing.

Yes, that’s my understanding too. I just didn’t want to go down that rabbit hole in my reply. :slight_smile:

I managed to get it to work. By using group members as triggers I was able to cut down the amount of rules. But I still have two separate rules that use the lambda and I don’t think I can resolve that.

Generally I am a little stumped that there is no simple way to use functions in these rules. I code a lot in Java where I do a lot of modularization through functions. This doesn’t seem possible in the rules DSL. For that reason I shortly tried to get the new Python scripting to work but I did not find a way to do it file based and have it show up as a rule instead of a script.

Anyway. Here is the finished rule. Maybe you see something else I could do to enhance it:

import org.openhab.core.model.script.ScriptServiceUtil

val checkAndSendAlert = [ NumberItem value, NumberItem minimum, NumberItem maximum, SwitchItem normal |
    if (normal.state == NULL)
    {
        sendCommand(normal, ON)
    }

    val sensor = transform("MAP", "alert.map", value.name)

    if ((value.state as Number) < (minimum.state as Number))
    {
        logInfo("alerts", "Value ({}) is below minimum ({})", value.state, minimum.state)
        if (normal.state == ON)
        {
            sendCommand(normal, OFF)
            val message = if (value.category.equals("temperature")) "⚡ %s %s at %.1f°C\nSensor is *below* minimum of %.1f°C" else "⚡ %s %s at %.0f%%%%\nSensor is *below* minimum of %.0f%%%%"

            val telegram = String::format(message, sensor, value.category, (value.state as DecimalType).floatValue, (minimum.state as DecimalType).floatValue)
            getActions("telegram", "telegram:telegramBot:myopenhabianbot").sendTelegram(telegram)
        }
    }
    else if ((value.state as Number) > (maximum.state as Number))
    {
        logInfo("alerts", "Value ({}) is above maximum ({})", value.state, maximum.state)
        if (normal.state == ON)
        {
            sendCommand(normal, OFF)
            val message = if (value.category.equals("temperature")) "⚡ %s %s at %.1f°C\nSensor is *above* maximum of %.1f°C" else "⚡ %s %s at %.0f%%%%\nSensor is *above* maximum of %.0f%%%%"
            val telegram = String::format(message, sensor, value.category, (value.state as DecimalType).floatValue, (maximum.state as DecimalType).floatValue)
            getActions("telegram", "telegram:telegramBot:myopenhabianbot").sendTelegram(telegram)
        }
    }
    else if (normal.state == OFF)
    {
        logInfo("alerts", "Value ({}) is back to normal ({}-{})", value.state, minimum.state, maximum.state)
        sendCommand(normal, ON)
        val message = if (value.category.equals("temperature")) "⚡ %s %s at %.1f°C\nSensor is back to *normal*" else "⚡ %s %s at %.0f%%%%\nSensor is back to *normal*"
        val telegram = String::format(message, sensor, value.category, (value.state as DecimalType).floatValue)
        getActions("telegram", "telegram:telegramBot:myopenhabianbot").sendTelegram(telegram)
    }
]

rule "A temperature or humidity sensor changed"
when
    Member of Temperature changed or
    Member of Humidity changed
then
    if (previousState != NULL) {
        val minimum = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name + "_Min") as NumberItem
        val maximum = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name + "_Max") as NumberItem
        val normal = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name + "_Normal") as SwitchItem
        checkAndSendAlert.apply(triggeringItem, minimum, maximum, normal)
    }
end

rule "A threshold changed"
when
    Member of Threshold changed
then
    val monitoredItemName = triggeringItem.name.substring(0, triggeringItem.name.lastIndexOf("_"))
    val monitoredItem = ScriptServiceUtil.getItemRegistry.getItem(monitoredItemName) as GenericItem
    val minimum = ScriptServiceUtil.getItemRegistry.getItem(monitoredItemName + "_Min") as NumberItem
    val maximum = ScriptServiceUtil.getItemRegistry.getItem(monitoredItemName + "_Max") as NumberItem
    val normal = ScriptServiceUtil.getItemRegistry.getItem(monitoredItemName + "_Normal") as SwitchItem
    checkAndSendAlert.apply(monitoredItem, minimum, maximum, normal)
end
1 Like

I’ve only ever used Python though the Helper Libraries which I think are not yet OH 3 compatible. But you can look at that code to see how to create a rule without using the Helper Library. When using the Helper Library a rule would look something like:

from core.rules import rule
from core.triggers import when
from core.utils import sendCommandCheckFirst
from configuration import weather_icon_path
import subprocess
from javax.imageio import ImageIO
from java.io import File

@rule("Is Cloudy", description="Generates an event when it's cloudy or not",
      tags=["weather"])
@when("Item vCloudiness changed")
def is_cloudy(event):
    """Sets a switch to ON when cloudiness gets above 50%."""

    newState = "ON" if items["vCloudiness"] > QuantityType(u"50.0 %") else "OFF"
    sendCommandCheckFirst("vIsCloudy", newState)

@rule("Weather Icon",
      description="Copy the current weather conditions icon",
      tags=["weather"])
@when("Item vWeather_Conditions_Icon changed")
@when("System started")
def cond_icon(event):
    """
    Download the weather conditions icon and convert it from gif to png.
    """

    cond_icon.log.info("Fetching the weather conditions icon... {}"
                       .format(ir.getItem("vWeather_Conditions_Icon").state))
    dl = subprocess.Popen(['/usr/bin/wget', '-qO-',
                  'http://argus:8080/rest/items/vWeather_Conditions_Icon/state'],
                  stdout=subprocess.PIPE)
    dd = subprocess.Popen(['/bin/dd', 'bs=22', 'skip=1'], stdin=dl.stdout,
                          stdout=subprocess.PIPE)
    dl.wait()
    f = open(weather_icon_path, "w")
    subprocess.call(['/usr/bin/base64', '-d'], stdout=f, stdin=dd.stdout)
    dd.wait()

This file is located in /etc/openhab/automation/jsr223/python/personal.

I don’t use JavaScript outside of UI created rules but I do have JavaScript libraries. You can find a discussion on how to do it in JavaScript at OH 3 Examples: Writing and using JavaScript Libraries in MainUI created Rules. I particularly point this post out because your lambda could be a library function that gets called.

Even way back in the OH 2.0 time frame I’ve been telling users who have even a little bit of experience with programming to not use Rules DSL. Rules DSL will not bend to work how you want it to. You’ll be much happier using any of the other languages.

Ok, so I have recently migrated to OH3 and I’m trying to figure out whether I’m better off using lambdas or migrating to jython functions or something else.

Basically, what I want to do is, instead of calling something like

myItem.sendCommand(ON)

I want to call

myFunction(myItem, ON)

and then have the function basically send the ON command, wait a minute, make sure that the command stuck, then either exit or try again and email me. If I did this in a rule it would be something like

function (receiveditem, receivedstate):
receiveditem.sendCommand(receivedstate)
while (receiveditem.state != receivedstate) {
  Thread::sleep(60000)
  receiveditem.sendCommand(receivedstate)
  mailActions.sendMail("email@address.com", "Command failed!", "blah blah blah")
}

^ not validated, but gives you the idea. I just have a few items that seem to flicker out for a bit and I need to try a second time to send a command.

It seems I can do this with a lambda, but I can’t tell from this thread whether lambdas might be outdated now. I was thinking this might be a good use for a script (write a script and call it from within a rule), but it doesn’t seem like scripts work like that…or that they can be passed inputs from a rule.

DSL skeleton without functions.

Put all to-be-monitored Items into a Group

Have a global Map (array) of timers keyed by Item name.
This is just to manage timers if already running.

Rule triggers from command to member of group.
Rule creates timer (after cancelling any existing) and stores in Map -
Timer to check if Item is in expected state
Timer to ring bells if not in expected state.
Timer clears self from Map when done.

The “reusable” part is the Timer, written once but there may be multiple copies running doing different things.

The basic process can be enhanced to re-try command before ringing bells.

What rossko57 suggested is what I would do for this situation. There is no need for a global lambda in this case because there will be just the one rule managing this.

So essentially the code that sends the commands will be normal. But you have another rule that triggers when the Item receives a command, sets the timer and when the timer goes off checks to see if the command “stuck”.

With this approach the code that handles this error case need not even be in the same .rules file as the other rules that command the Item. It will also work if the Item is commanded from the UI whereas the lambda approach would only work for commands made from Rules.

Is there a reason to use a timer array instead of Thread::sleep(60000)? The only reason I can think of to go through the hassle of timers (and I do find timers to be a hassle to debug) is if the following steps occur:

  1. item1 receives command
  2. check rule sleep begins
  3. before check rule sleep ends, item2 receives command
  4. check rule starts over, basically killing the sleep for item1
  5. item1 never gets checked

I had thought that multiple instances of a rule could be running at once such that the above steps wouldn’t happen (specifically, step 4 would never happen because it would just start a second instance of the check rule), but what do I know.

I have a few rules with timers, but they are usually set on the order of hours or involve delays of a variable time (e.g., delay until a time of day). When I have short, fixed delays in my rules, I just use sleep.

  • In OH 2 only five rules can be running at a give time. With a thread sleep that means for a whole minute 20% of your capacity to run rules is unavailable. If that rule were to trigger four more times in that minute no other rules will be able to run.
  1. item1 receives command
  2. check rule sleep begins
  3. item2 receives command
  4. check rule sleep begins
  5. item3 receives command
  6. check rule sleep begins
  7. item 4 receives command
  8. check rule sleep begins
  9. item5 receives command
  10. check rule sleep begins
  11. any other rule trigger event occurs, all threads are in use so the rule either has to wait for one of the above rules to complete, and if it takes too long the event is dropped. Once you run out of threads there is a huge cascade of events backing up and your rules eventually just stop.
  • In OH 3 the problems is less dire but still a potential problem. Only one instance of a rule can run at a time. Subsequent triggers of the rule will be queued up and worked off in sequence in order. If you have three triggers of the rule within that first minute, it can be up to three minutes before that that third trigger of the rule starts to process the event.
  1. item1 receives command
  2. check rule sleep begins
  3. item2 receives command
  4. item3 receives command
  5. up to a minute passes and the rule triggered on 2 exits
  6. check rule sleep begins for the event on line 3
  7. a minute passes and the rule triggered on line 3 exits
  8. check rule sleep begins for the event on line 4

It’s a minute before the event on line 3 starts processing and two minutes before the event on line 4 starts processing.

  • It is impossible to cancel a sleep (not strictly speaking true but for all practical purposes it’s impossible to do so in rules) So your 4 would never happen in any circumstances.

Your understanding on how rules trigger is therefore incorrect.

A Timer should always be the first tool you reach for with openHAB rules. Not just reserved for really long times. The rule of thumb is anything longer than 200-300 msec should be in a Timer.

Woah. Good to know. Thanks for the thorough explanation.

Can I use lambdas in OH3 using the UI based definition or do I need to define rules text files?
If this is only possible using text files would there be a possibility defining functions for ECMAscript 2021 rules in the UI?

You can define them but in the UI there is no “outside” the rule to put it. So there’s not much use in then outside of timers, filters and such. In 3.4 there a new cache which I think is available in Rules DSL but I don’t know what it will do with Rules DSL lambdas.

For the most part, if you want to create a library of functions, you are better off using another language.

But really. If you want reusable functions, you are taking about creating a personal library. There are instructions for how to do this in the docs. But it can’t be done through the UI.

What can be done is one rule can call another one. But there are limitations there, such as no way to get a return value.

In ECMAscript 2021 you can make a function inside the rule in the UI and then call it.

function DoSomething(a, b) {
 // Do something
}

var a = '1';
var b = '2';
DoSomething(a, b);
a = '3';
b = '4';
DoSomething(a, b);

But the variables seem to be taken by reference, not value.

Anyway this is perfect, exactly what I need.

if that’s all you want, you can do that in Rules DSL too. It’s only a problem when you have more than one script that needs the function/lambda. Then the function/lambda needs to be defined outside the rule.