Reusable Functions: A simple lambda example with copious notes

Jyton. I have not heard about it. I asked google and it looks like another language and there is not much examples how to use it.
Maybe u know a good tutorial or thread here with explanation?

Thanks!

You can use the new rule engine and do scripted automation using Jython, which is Python for the JVM. The OH docs are a bit meager. It has some important information, but don’t follow those installation instructions. The raw automation API is extremely rough to use without living in the source code. Fortunately, there are helper libraries that make it very easy to use and are a game changer for OH automation. There is a lot of documentation to help and most design patterns now also have Jython examples. The installation isn’t too difficult, but I’m working on an addon to simplify things. Even @rlkoshak has migrated all of his rules from the rules DSL to Jython and @vzorglub is in process.

https://openhab-scripters.github.io/openhab-helper-libraries/index.html

Let me know if you have an questions or issues… but probably should be in a new topic :slightly_smiling_face:.

If you post your logging messages to Item states, these are common to all rules, whatever their source - xxx.rules files, Jython, etc.

This is demonstrated in Design Pattern: Separation of Behaviors

Do lambas go in .rules files?

I ask because the example is giving me validation warnings with OH2.5M5:

2019-11-26 08:15:26.434 [WARN ] [el.core.internal.ModelRepositoryImpl] - Configuration model 'light.rules' has errors, therefore ignoring it: [97,1]: missing EOF at 'val'

TIA

Yes, but they must go at the top of the file before all your Rules. This error is coming from line 97 which means your lambda is likely after the definition of a Rule or the error is coming from a Rule.

I should note that global lambdas like this are a code smell in Rules DSL. They can be handy in a very small set of circumstances, but they have sever limitations that make them dangerous to use. For example, they are not thread safe. If it gets called twice the variables from the first call will become overwritten by the second call. Also, you cannot call the lambda outside of the .rules file it is defined in. There is almost always a better way. For example: Design Pattern: Separation of Behaviors

@rlkoshak

I’m looking into lamdas and found your post. Is it worth rewriting to include notes for JRS223 Python/Jython?

Maybe there’s a good design pattern article. I’m still searching

Regards,
Burzin

Functions are available as a basic part of the Scripted Automation languages. If you take a week long course on Python, functions will be introduced somewhere on the morning of the first day. I didn’t include Python or JavaScript in the above DP because, for all purposes, the above DP doesn’t apply to Scripted Automation. Use what ever is available for the language chosen.

Using lambdas in this way is a work around for the fact that Rules DSL doesn’t support functions. There is no such limitation in any of the other languages.

3 Likes

@rlkoshak is it possible that lambdas are no longer supported in Openhab 3? I upgraded yesterday and since have been trying to get a rule to work again. When changing reloading the model it says:

2021-01-04 17:15:47.814 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'alert.rules', using it anyway:

The field Tmp_alertRules.checkAndSendAlert refers to the missing type Object

When triggering the rule it just says:

2021-01-04 17:15:47.770 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'alert-1' failed: null in alert

Check the section concerning Rules here

Sadly, I did not find anything regarding lambdas there.

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