Rule to send alarms at intervals based on value/range of item

Hello,
I wonder if anyone can assist me create a rule that would send an alarm message by publishing to an MQTT topic when the humidity reaches a certain threashold and then repeat the alarm every hour as long as humidity remains in that specific range/threashold

For example,
Send an alarm when humidity reaches 60+% and continue to send the alarm every 60 minutes as long as the humidity is within 60-70%. If it goes above 70% then publish to a different MQTT topic but this time send the alarm once every 15 minutes.

Given a number item myHumidity and string Items myHumWarn and myHumCrit, the rule would be something like that:

var Timer tHum = null
var Boolean tHumCrit = false

rule "humidity alarm"
when
    Item myHumidity changed  //humidity changed
then
    if (myHumidity.state instanceof DecimalType) {  //is a number?
        if((myHumidity.state as DecimalType).intValue > 59 && myHumidity.state as DecimalType).intValue < 70 ) { //warning level
            if (tHumCrit) { //was critical?
                tHumCrit = false  //not any longer
                tHum.cancel
                tHum = null
            }
            if(tHum == null) {  //timer started?
                tHum = createTimer(now.plusSeconds(1),[|
                    myHumWarn.sendCommand("Humidity Alert!")
                    tHum.reSchedule(now.plusHours(1))
                ])
            }
        }
        else
        if((myHumidity.state as DecimalType).intValue > 69 ) {  //critical level
            if(tHumCrit != true) {  //was not critical?
                tHumCrit = true  
                tHum.cancel    //then set new timer...
                tHum = createTimer(now.plusSeconds(1),[|
                    myHumCrit.sendCommand("Humidity Alert Critical!")
                    tHum.reSchedule(now.plusMinutes(15))
                ])
            }
        }
        else
            if(tHum != null) { //timer running?
                tHum.cancel  // cancel timer
                tHum = null
            }
    }
    else
        logInfo("hum_alarm","myHumidity state error!")  //value was not of type number
end

The idea is, to trigger the rule, if the item changed, then get the range, catch, if timer is still running, otherwise start timer with a short delay, when timer expires, send the message and reschedule the timer with 1 hour or rather 15 Minutes.
If not in range, cancel timer, so the messages will nor reappear.

This code is not tested, so there may be issues…

A slightly simpler solution using the Expire Binding:

// same Items as Udo plus
Switch myHumWarn_Timer { expire="60m,command=OFF" }
Switch myHumCrit_Timer { expire="15m,command=OFF" }
import org.eclipse.xtext.xbase.lib.Functions

val Functions$Function<Boolean> setTimers = [|

    val hum = myHumidity.state as Number

    var zone = "SAFE"
    if(hum > 70) zone = "CRITICAL"
    else if(hum >=60 && hum < 70) zone = "WARN"

    switch zone{
        case "SAFE": {
            myHumCrit_Timer.postUpdate(OFF)
            myHumWarn_Timer.postUpdate(OFF)            
        }
        case "WARN": {
            myHumCrit_Timer.postUpdate(OFF)
            if(myHumWarn_Timer.state != ON) myHumWarn_Timer.sendCommand(ON)
        }
        case "CRITICAL": {
            if(myHumCrit_Timer.state != ON) myHumCrit_Timer.sendCommand(ON)
            myHumWarn_Timer.postUpdate(OFF)
        }
    }
    true
]

rule "humidity changed"
when
    Item myHumidity changed
then
    setTimers.apply()
end

rule "myHumCrit_Timer"
when
    Item myHumCrit_Timer received command
then
    if(receivedCommand == ON)  myHumCrit.sendCommand("Humidity Alert Critical!")
    else setTimers.apply()
end

rule "myHumWarn_Timer"
when
    Item myHumWarn_Timer received command
then
    if(receivedCommand == ON) myHumWarn.sendCommand("Humidity Alert!")
    else setTimers.apply()
end

Theory of operation.

When the humidity gets into an alerting zone, the appropriate Timer Item is sent the ON command. If the humidity falls out of a danger zone, the appropriate Timer Item is post updated the OFF state. Using postUpdate will cancel the Expire Timer without triggering the received command rules.

The received command rules check to see if the command was ON, in which case it sends the alert message. If the command was OFF it means the Expire binding was the source so it is time to send the alert again but only if we are still in the danger zone.

OK, I admit this really isn’t any fewer lines of code but I maintain it is a little simpler because there isn’t as much Timer book keeping code. But it did introduce the lambda to keep from repeating code so maybe its a tossup.

Anyway, alternaitves are good. :slight_smile:

3 Likes

Thank you both very much indeed for your prompt reply and apologies for not replying sooner. I have to admit that so far I have implemented @Udo_Hartmann rule and haven’t had a chance to try @rlkoshak though I’m grateful to both of you for your time.

It took me longer to reply because it wasn’t working and I was keen to debug the issue myself which was quite useful as I learnt a lot more doing so than I would have if I had simply replied to say that it wasn’t working. Anyway, I managed to narrow it down to the tHum.reSchedule having a capital S. Changing it to tHum.reschedule fixed the issue (if anyone wants to use this rule be mindful of this) :smile:

Hi guys,
Just playing with the code to trigger a critical event and I’m getting “Rule ‘humidity alarm’: cannot invoke method public abstract boolean org.eclipse.smarthome.model.script.actions.Timer.cancel() on null” when the second part of the code is executed

else
    if((myHumidity.state as DecimalType).intValue > 69 ) {  //critical level
        if(tHumCrit != true) {  //was not critical?
            tHumCrit = true  
            **tHum.cancel**    //then set new timer...
            tHum = createTimer(now.plusSeconds(1),[|
                myHumCrit.sendCommand("Humidity Alert Critical!")
                tHum.reSchedule(now.plusMinutes(15))
            ])
        }
    }

Any ideas?

The Timer is null. Null is a special state one can set a variable to mean uninitialize. Almost any variable can be set to null.

Null has no methods nor did it have a value. About all you can do with null is test to see if your variable is null before doing anything with it.

So either add a

if(tHum != null) tHum.cancel

Or you can use a short cut built into the language that means the same thing:

tHum?.cancel

In my code, I did not take care of a humidity level over 69% AND no timer started, because the timer should have been start when humidity rose over 59%. But of course, if testing with values set by hand, this would be a possible condition…

That did the trick. Many thanks

I tried to implement your rule, but it does not work. is it coded for OH2.2? I’m on 2.3.
the errormsg:

2018-08-22 22:00:20.226 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'humidity.rules', using it anyway:
The import 'org.eclipse.xtext.xbase.lib.Functions' is never used.
The field Tmp_humidityRules.setTimers refers to the missing type Object
The field Tmp_humidityRules.setTimers refers to the missing type Object
The field Tmp_humidityRules.setTimers refers to the missing type Object

somewhere I read that OH2.3. does not need any imports anymore?
however, when the humidity rule triggers I get this messages:

2018-08-22 21:57:58.669 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'humidity changed': 'apply' is not a member of 'Object'; line 30, column 5, length 17

humidity.rules

import org.eclipse.xtext.xbase.lib.Functions
val Functions$Function<Boolean> setTimers = [|

    val hum = LED_Bad_hum.state as Number

    var zone = "SAFE"
    if(hum > 60) zone = "CRITICAL"
    else if(hum >=50 && hum < 60) zone = "WARN"

    switch zone{
        case "SAFE": {
            LED_Bad_humWarn_Timer.postUpdate(OFF)
            LED_Bad_humCrit_Timer.postUpdate(OFF)            
        }
        case "WARN": {
            LED_Bad_humCrit_Timer.postUpdate(OFF)
            if(LED_Bad_humWarn_Timer.state != ON) LED_Bad_humWarn_Timer.sendCommand(ON)
        }
        case "CRITICAL": {
            if(LED_Bad_humCrit_Timer.state != ON) LED_Bad_humCrit_Timer.sendCommand(ON)
            LED_Bad_humWarn_Timer.postUpdate(OFF)
        }
    }
    true
]

rule "humidity changed"
when
    Item LED_Bad_hum changed
then
    setTimers.apply()
end

rule "LED_Bad_humCrit_Timer"
when
    Item LED_Bad_humCrit_Timer received command
then
    if(receivedCommand == ON)  LED_Bad_humCrit.sendCommand("Humidity Critical Alert!")
    else setTimers.apply()
end

rule "LED_Bad_humWarn_Timer"
when
    Item LED_Bad_humWarn_Timer received command
then
    if(receivedCommand == ON) LED_Bad_humWarn.sendCommand("Humidity Warning Alert!")
    else setTimers.apply()
end

I’m sure someone knows whats wrong… thanks

Those are warnings, not errors.

If you define the lambda like this you do need the import. The fact that it is saying that the import isn’t being used implies there is a typo or something in the definition. But you can define the lambda without needing to refer to Functions at all:

val setTimers = [ |
    // blah blah blah

    // don't put true as the last line of the lambda, it isn't needed
]

I don’t know what is wrong but that should at least address the warning about the import since you no longer need the import when you use this syntax.

1 Like

thanks, Rich. deleting the imports solved the problem. rules are working…

I tried to expand the rule for a group of humidity items. Finally I had to avoid lambdas because I ran into troubles to handle triggeringItems status inside the lambda code…

this is what I have coded now:

val hum = 0

rule "humidity changed"
when
    Member of gHum changed
then
    val hum = triggeringItem.state as Number                    //get humidity level from Item
    val timerWarning = triggeringItem.name +"_humWarn_Timer"    //append the triggering Item name with "_humWarn_Timer" so I can select the right timer. unfortunately this val is a string
    val timerWarn = gHumTimer.members.findFirst[ i | i.name == timerWarning]  //find the above string in the group. as the members of the group are switches, I can use sendCommand to shoot the timer
    val timerCritical = triggeringItem.name +"_humCrit_Timer"   //same procedure as for warning alerts
    val timerCrit = gHumTimer.members.findFirst[ i | i.name == timerCritical]
        
    var zone = "SAFE"
    if(hum > 80) zone = "CRITICAL"
    else if(hum >=70 && hum < 80) zone = "WARN"

    switch zone{
        case "SAFE": {
            timerWarn.postUpdate(OFF)
            timerCrit.postUpdate(OFF)            
        }
        case "WARN": {
            timerCrit.postUpdate(OFF)
            if(timerWarn.state != ON) timerWarn.sendCommand(ON)
        }
        case "CRITICAL": {
            if(timerCrit.state != ON) timerCrit.sendCommand(ON)
            timerWarn.postUpdate(OFF)
        }
    }
    Thread::sleep(250)
    logInfo("settimer", timerWarning +": " + timerWarn.state.toString + ", " +timerCritical +": " + timerCrit.state.toString +", " + hum + "%")
end


rule "humidity Alert notification"
when
    Member of gHumTimer received command
then
    if(receivedCommand == ON) {
        val alert = transform("MAP", "humidity.map", triggeringItem.name.toString) //just to transform the timer item into a more human readable form
        val item = transform("MAP", "humidity.map", alert) //to find which humidity item is related to the Timer that got ON
        val hum = gHum.members.findFirst[ i | i.name == item] //to transform the above string "item" into a openhab item and get the humidity level
        var String humalert = alert + ": " + hum.state + "%"
        humidityAlert.sendCommand(humalert)
        sendNotification("m.xxxxxxx@gmx.at", "Luftfeuchtigkeit: " + alert + ": " + hum.state + "%")
            }
 end

first tests were successfull.
one difference to the lambda code from the previous posts is that as soon as the timers expire the rule immediately restarts the setTimers rule, that said as long as the humidity values are too high the warning or critical alert is ON. my code requires to receive an item update to re-activate the alerts, much less elegant I would say.
do you see any improvements I can do in that rules?

Except for the sleep there isn’t anything that staffs out. I would probably use

if(receivedCommand == OFF) return;

which will let you avoid indenting all of the rest of the lines.

As far as I know, it’s not allowed to redefine a constant. Am I wrong?

first line (outside a rule):

val hum = 0

And in both rules:

val hum = triggeringItem.state as Number //get humidity level from Item

and

val hum = gHum.members.findFirst[ i | i.name == item] //to transform the above string "item" into a openhab item and get the humidity level

You are correct. You cannot assign a new value to a val. Good catch. Of course VSCode would have told us that immediately as well.

thanks, I changed the constants to be different in every rule.
neither VSC nor the eventlogs did complain about that…