Timers in functions not possible?

Hello,

to reduce redundancy in my rules code, I have decided to put things in functions to make things also easier to maintain. Now I have a function that is used for each of my three roof rollershutters to close them and checks, if the corresponding window is closed and if not creates a wait timer for the shutter, closes the window and then the timer closes the shutter. IN THEORY :wink:

To me it seems that timers and functions are a bit tricky to use. I have created a small code example to demonstrate my issue:

var Timer myTimer = null

val Functions.Function2<String, String, void> myFunction = [ String a, String b |
	logInfo("TEST", "Timer: " + myTimer)
	if (myTimer === null) {
		myTimer = createTimer(now.plusSeconds(5), [|
			logInfo("TEST", "Printing String a: " + a)
		])
		logInfo("TEST", "Printing String b: " + b)
	}
	return null
]

rule "TimerTest"
when
	Time cron "*/10 * * * * ?"
then
	logInfo("TEST", "Before function call. Timer: " + myTimer)
	myFunction.apply("Hello Timer!", "Hello World!")
	logInfo("TEST", "After function call. Timer: " + myTimer)
end

which I have in a file test.rules. The behavior is though rather unexpected as neither the after function call nor the b-string are logged, i.e., they seem not being executed:

# tail -n 50 -f /var/log/openhab2/*log  | grep TEST
2018-08-24 10:06:40.071 [INFO ] [.eclipse.smarthome.model.script.TEST] - Before function call. Timer: null
2018-08-24 10:06:40.071 [INFO ] [.eclipse.smarthome.model.script.TEST] - Timer: null
2018-08-24 10:06:45.072 [INFO ] [.eclipse.smarthome.model.script.TEST] - Printing String a: Hello Timer!
2018-08-24 10:06:50.003 [INFO ] [.eclipse.smarthome.model.script.TEST] - Before function call. Timer: null
2018-08-24 10:06:50.005 [INFO ] [.eclipse.smarthome.model.script.TEST] - Timer: null
2018-08-24 10:06:55.006 [INFO ] [.eclipse.smarthome.model.script.TEST] - Printing String a: Hello Timer!
2018-08-24 10:07:00.020 [INFO ] [.eclipse.smarthome.model.script.TEST] - Before function call. Timer: null
2018-08-24 10:07:00.021 [INFO ] [.eclipse.smarthome.model.script.TEST] - Timer: null
2018-08-24 10:07:05.021 [INFO ] [.eclipse.smarthome.model.script.TEST] - Printing String a: Hello Timer!
2018-08-24 10:07:10.001 [INFO ] [.eclipse.smarthome.model.script.TEST] - Before function call. Timer: null
2018-08-24 10:07:10.002 [INFO ] [.eclipse.smarthome.model.script.TEST] - Timer: null

After the “Timer: null” output, I get the following error message that I do not understand:

2018-08-24 10:25:40.008 [ERROR] [ntime.internal.engine.ExecuteRuleJob] - Error during the execution of rule 'TimerTest': Cannot assign a value in null context.

What is especially weird to me is that the timer is obviously created and executed but the line after that never executed.

I hope somebody can help me a) understand what’s going wrong there and b) help fix the issue :slight_smile:

didn’t even know that functions can be used :wink: thanks for the hint.

I ran some tests using your example and found out that the “global variables” on the top won’t be recognized inside the function. maybe that’s your issue. The solution could be to pass the timer as method parameter by reference, but I’m not sure if this is possible at all and limited to ‘by value’

I changed your example and turned the global timer into a global string, it was never recognized inside the function.

I tried the timer as parameter, but it seems that it’s immutable, i.e., the function takes it as constant and does not allow to do a timer = createTimer…

As someone who has been there and done that, I suspect you will be happier with the results if you instead focusing on making your Rules generic instead of using lambdas. There are a number of limitations with lambdas that make them awkward to work with including:

  • they can only be used from the one .rules file they are defined in
  • they cannot see other global variables meaning you need to pass lambdas you want your lambda to call as an argument (I may have just thought of a workaround for that but it’s ugly so won’t mention it here unless someone asks)
  • errors generated from lambdas are inscrutable and usually relatively meaningless
  • they only support seven arguments

But if you make your Rules generic you avoid duplicated code and the Rules themselves define the separation of the code. Rules can be triggered from any .rules file (depending on how they are triggered). And your Rules code will usually end up shorter.

Anyway, off of my soap box, to your actual question…

That’s because you have to pass myTimer as an argument to myFunction. Global vars and vals are not in scope with eachother (i.e. cannot see each other).

I’m actually surprised it isn’t complaining that myTimer is unknown from the lambda and actually seems to be successfully using it. But that is why it is not getting to the second log statement. It manages to create the Timer but runs into an error attempting to assign it to myTimer since the lambda doesn’t have access to that variable. So it throws and error before it can run the second log statement.

OK, there it is. As you can see, and as I mentioned above, errors generated from inside lambdas are usually inscrutable and unhelpful.

Assuming you are on OH 2.3, there is a slightly cleaner way to define a lambda.

val myFunction = [ String a, String b, Timer = myTimer |
	logInfo("TEST", "Timer: " + myTimer)
	if (myTimer === null) {
		myTimer = createTimer(now.plusSeconds(5), [|
			logInfo("TEST", "Printing String a: " + a)
		])
		logInfo("TEST", "Printing String b: " + b)
	}
]

Now another warning I have is I don’t know if the arguments are passed by reference or by value. So when you use myTimer = createtimer(... I don’t know if the global myTimer gets assigned the new Timer, or if only the local myTimer gets assigned the new Timer and that local myTimer goes away once the lambda exits. I’ve only ever written lambdas that work with collections of Timers so I’ve never had to figure that out.

Oops, @StefanH beat me to it. I’m basically saying the same thing only with a lot more words.

Anyway, I recommend looking at the Design Patterns. In particular:

With these DPs you should be able to achieve your goals without using lambdas.

2 Likes