Rule help to trigger if value repeats a certain number of times

Hello, i’m trying to make a rule to send a notification if an item stabilises on a value (ie if it repeats for say 10 times the same value)

I’ve made a little led display to indicate if it is hotter outside than inside or the same or cooler or much cooler outside, and so my device i made displays and returns a colour code via mqtt to openhab.

So typically during summer in the morning, the values start from blue (cold) to green (similar outside and inside) and then to orange and red. I want to be able to send an alert as it passes from each of those phases, but typically as the values approach the threshold they will jump around for a little while say from blue to green to blue to green and then stabilise on green as it moves away from the threshold.

I’ve tried to modify a rule from one of the examples like this…

```php
rule "really changed?" // stability check
//////////////////////////////////////////////////////////////////////////////////////////////////
when
     Item Esp_Colour changed
then
	//logInfo("colourstate=", "Newcolour:" +Esp_Colour.state)
    var NewColour = Esp_Colour.state.toString()
	logInfo("colourstate=", NewColour + " AND " +Esp_Colour.state)
	if(Esp_Colour.state.toString()==NewColour) {
            colourtimer = createTimer(now.plusMinutes(20)) [|
			sendNotification("my_email" , "New colour = " +Esp_Colour.state)
			logInfo("colourstate=", " COLOURSTATE MATCH ")
			]
        } else {
            if(colourtimer!=null) {
                colourtimer.cancel
                colourtimer = null
				logInfo("colourstate=", "TIMER CANCELLED BY CHANGE")
            }
        }
    end

any pointers or suggestions? it does send alerts, but not after the 20 minutes of stable results that i want, but sometimes every 2 or 3 minutes…and it never seems to ‘Else’ to the timer cancel stage…

I’ve thought about a different type of rule with a counter variable, but i’m not sure what the best approach should be…

Thanks in advance for any help

I think the easiest way is to create a counter item every time your color is updated you increase the value if it reaches a certain value 5 or something you do your action and reset it to 0.

Another solution is to use persistance. you persist ervery update and at the desired point you retrieve the last stae with a different value from this value you get the time and compare it with the current time if it is greater than … then yu are able to do some thing.

Thomas

This one is a little tricky. I think the Latch design pattern would be the best approach. This looks like what you are trying to do. There are a couple of ways to implement this. I would probably use a timer.

var Timer colourtimer = null

rule "really changed?"
when
    Item Esp_Colour changed
then
    if(colourtimer == null || colourtimer.hasTerminated) {
        sendNotification("my_email", "New colour = " + Esp_Colour.state.toString)
        colourtimer = createTimer(now.plusMinutes(20), [|
            colourtimer = null
        ])
    }        
end

Theory of operation: If there is a currently running colourtimer, simply ignore all incoming color changes for 20 minutes. Otherwise send a notification and start colourtimer. The alert will be sent on the first change of the Esp_colour and ignore any other changes for the next 20 minutes. After the 20 minutes the assumption is the temp will have stabilized and still be the same color. The existence of the colourtimer is acting as a latch.

This isn’t quite what you are asking for but it is the simplest approach I can think of.

[Edit] Removed a Persistence based rule which upon review would not have worked.

The major error in your existing rule is that you set NewColour to Esp_Colour.state.toString and then immediately compare Esp_Colour.state.toString to NewColour. That condition will ALWAYS return true so that is why the else never executes. I think what you really want to do is define NewColour as a global var and only change it inside the if clause, not before. But even then this comparison is redundant because you already know that Esp_Colour has changed because of the rule trigger so there really is no need for NewColour or the if/else at all. The very fact that you are running the rule tells you the colour has changed.

But, since the rule only executes when the colour changes what you would really need to do is reschedule the Timer in the rule every time the colour changes. This will cause the timer to only go off after the colour has remained the same for 20 minutes. So here is a third approach using a Timer.

var Timer colourtimer = null

rule "really changed?"
when
    Item Esp_Colour changed
then
    if(colourtimer == null || coulourtimer.hasTerminated){
        colourtimer = createTimer(now.plusMinutes(20), [|
            sendNotification("my_email" , "New colour = " +Esp_Colour.state)
        ])
    }
    else {
        colourtimer.reschedule(now.plusMinutes(20))
    }
end

I would not use a counter to solve this because you have no guarantee how many times it will switch between one color and the next while it is at that transitional area.

Yet another approach would be to set a buffer range when changing the color of the light to avoid the rapid flapping. That would eliminate the bouncing entirely and is probably the “correct” solution. Your rule that sets Esp_Colour would look something like:

    var Number difference = (OutsideTemp.state as DecimalType) - (InsideTemp.state as DecimalType)
    var String newColour = Esp_Colour.state.toString
    switch(newColour){
        case "blue": { // current state is blue
            if(difference >= -1.5) newColour = "green"
        }
        case "green": { // current state is green
            if(difference <= -2) newColour = "blue"
            else if(difference >= 2) newColour = "orange"
        }
        case "orange": { // current state is orange
          if(difference <= 1.5) newColour = "green"
          else if(difference >= 5) newColour = "red"
        }
        case "red": { // current state is red
            if(difference <= 4.5) newColour = "orange"
        }
    }
    if(Esp_Colour.state.toString != newColour) EspColour.sendCommand(newColour)

Theory of Operation: There is a half a degree (assuming degrees C given your spelling of “colour”) buffer between colors. So, for example, if the temperature is going up it will transition from “blue” to “green” when the difference in temperatures is -1.5. But if the temperature is dropping, it will only transition from “green” to “blue” when the difference is -2. As long as the temperature fluctuations are less than half a degree it won’t cause the colour to keep changing and there is need for the rules above at all. Furthermore, this will enforce the state transitions between the colours (i.e. it can’t jump from blue to orange without going through a green state first).

You may need to experiment with how big or small you can make the buffer to avoid the bouncing. Just make the buffer larger than the typical jump you see between temperature readings and this should work.

This has been fun to think about. I need to turn these into more Design Patterns.

2 Likes

@Dibbler42 Thanks for this reply, this was my first instinct, for the counter, but i just couldn’t wrap my head around some of these rule syntax. I’ve just started my vacation, so i might try to do it as a counter and as Rich is suggesting in the following post and see which works best, or if there is anything to choose from the two.

@rlkoshak Thanks again for this response to another one of my half baked rules. i think that’s the 4th time now that you’ve proposed a great solution or solutions!

It will take me some time to process what you’ve written, but i’m here to learn.

Just as a side note, there is some buffering between zones, it is handled on the display device,(esp8266 with a few leds)
i think they each have a range of 2 - 5 degrees C. (yes you are correct about my location)
I only send the calculated value from the two temp sensors to the esp node, and it evaluates the colour and actually feeds this back again to openhab. Inelegant i’m sure in some ways, but it works well.

As before, really appreciated yoyr time and considered answers. (More examples of rules on the wiki would always be appreciated by me if thats what your referring to)

1 Like

Actually I’m referring to this thread, which is a holding place until I can get these included as part of the OH 2 documentation.

Hello again,

So i tested both the timer based rules, but it’s not quite perfect…

I’m finding some conditions where the rule is sending me a notification, and it’s just an update of the same previous colour.

I think whats happening is that the same colour is qualifying through the rules, even though it was the same colour that triggered it the last time,

So i have been trying to set another condition, to check if it’s the same colour that previously qualified, but i can’t just get the syntax right

I tried some variations around:

var string real_colour
var Timer colourtimer = null

rule "really changed?"
when
    Item Esp_Colour changed											
then
    if(Esp_Colour.state != real_colour) && (colourtimer == null || colourtimer.hasTerminated)){          
        real_colour = Esp_Colour.state
		colourtimer = createTimer(now.plusMinutes(20), [|          
            sendNotification("crightonsetchfield@gmail.com" , "NEW COLOUR = " +Esp_Colour.state)  
        ])
    }
    else {
        colourtimer.reschedule(now.plusMinutes(20))				
    }
end

What would be the correct way to write this?

For one:

val String real_colour

(Notice the capitalized “String”).

Second:

real_colour = Esp_Colour.state.toString

Finally, just create a log statement to printthe current color and the last color in the logs. Don’t change the behavior of the rule until you understand what is really going on. I say this because what you are describing is not possible. When using the “changed” rule trigger, the rule will only trigger when the new state is different from the last state. Are you certain that even though the String that Esp_Colour is being set to is identical each time? For example, “red” does not equal "red ", does not equal " red ", does not equal “red\r” and so on. So if the white space is not consistent or you have captialized sometimes but not other times all will be treated as a change and fire the rule.

Unless there is some odd behavior with String Items that I don’t know about.

Hi, Good point about the white spaces, i had put logfile feedback, i found that it was working correctly, but my guess which completely could be wrong was that the rule was running, arriving with a colour and publishing it to my phone(say BLUE), then an errant value was triggering the rule but getting cancelled before the 20 minute time(Lets say GREEN), and then the original correct colour was triggering the rule again… (BLUE)

So the rule was working great, but i just thought that adding an additional check to see if the above situation wasn’t happening.

But as it stands in designer i get an error with the line:
if(Esp_Colour.state != real_colour) && (colourtimer == null || colourtimer.hasTerminated)){
no viable alternaive at ‘&&’

and in the logfile:

[ERROR] [o.o.c.s.ScriptExecutionThread :50   ] - Error during the execution of rule 'really changed_2?': Ambiguous methods [protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateIfExpression(org.eclipse.xtext.xbase.XIfExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateInstanceOf(org.eclipse.xtext.xbase.XInstanceOfExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateMemberFeatureCall(org.eclipse.xtext.xbase.XMemberFeatureCall,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateNullLiteral(org.eclipse.xtext.xbase.XNullLiteral,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateNumberLiteral(org.eclipse.xtext.xbase.XNumberLiteral,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateReturnExpression(org.eclipse.xtext.xbase.XReturnExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateStringLiteral(org.eclipse.xtext.xbase.XStringLiteral,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateSwitchExpression(org.eclipse.xtext.xbase.XSwitchExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateThrowExpression(org.eclipse.xtext.xbase.XThrowExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateTryCatchFinallyExpression(org.eclipse.xtext.xbase.XTryCatchFinallyExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateTypeLiteral(org.eclipse.xtext.xbase.XTypeLiteral,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateVariableDeclaration(org.eclipse.xtext.xbase.XVariableDeclaration,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateWhileExpression(org.eclipse.xtext.xbase.XWhileExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateAbstractFeatureCall(org.eclipse.xtext.xbase.XAbstractFeatureCall,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateAssignment(org.eclipse.xtext.xbase.XAssignment,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateBlockExpression(org.eclipse.xtext.xbase.XBlockExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateBooleanLiteral(org.eclipse.xtext.xbase.XBooleanLiteral,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateCastedExpression(org.eclipse.xtext.xbase.XCastedExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateClosure(org.eclipse.xtext.xbase.XClosure,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateConstructorCall(org.eclipse.xtext.xbase.XConstructorCall,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected org.eclipse.xtext.xbase.interpreter.IEvaluationResult org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateDoWhileExpression(org.eclipse.xtext.xbase.XDoWhileExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator), protected java.lang.Object org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateForLoopExpression(org.eclipse.xtext.xbase.XForLoopExpression,org.eclipse.xtext.xbase.interpreter.IEvaluationContext,org.eclipse.xtext.util.CancelIndicator)] for params [null, org.eclipse.xtext.xbase.interpreter.impl.DefaultEvaluationContext@1c368f2, org.eclipse.xtext.util.CancelIndicator$1@1a4230d]

Found missing ( at start of
if(Esp_Colour.state != real_colour)