Reusable Functions: A simple lambda example with copious notes

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.