Displaying time since last update of an item

I want to display an item that shows how long ago particular item was updated. I have a Number item that stores the actual time since last update

Number 	  SinceLastUpdate

And I have a rule:

rule Update
when 
	Item SinceLastUpdate received update
then
	logInfo("Debug","SinceLastUpdate received update: " + SinceLastUpdate.state)

	myTimer = createTimer(now.plusSeconds(5), [ |
		logInfo("Debug","Timer ticked " + SinceLastUpdate.state)
		val newUpdate = SinceLastUpdate.state;
		logInfo("Debug","new update " + newUpdate)
		SinceLastUpdate.postUpdate(newUpdate)
	])	
end

When another item receives an update it posts update to the item SinceLastUpdate

	SinceLastUpdate.postUpdate(0)

myTimer is defined as global (at the start of rules file)

var Timer myTimer

The solution described above works but it does not increase the value of newUpdate. That’s where I struggle. When I change the line val newUpdate = SinceLastUpdate.state to

val newUpdate = SinceLastUpdate.state + 5

I get this in log:

2018-09-28 10:49:42.271 [INFO ] [eclipse.smarthome.model.script.Debug] - SinceLastUpdate received update: 0
2018-09-28 10:49:47.273 [INFO ] [eclipse.smarthome.model.script.Debug] - Timer ticked 0
2018-09-28 10:49:47.274 [ERROR] [org.quartz.core.JobRunShell         ] - Job DEFAULT.2018-09-28T10:49:47.272+02:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  logInfo(<XStringLiteralImpl>,<XBinaryOperationImplCustom>)
  val newUpdate
  logInfo(<XStringLiteralImpl>,<XBinaryOperationImplCustom>)
  <XFeatureCallImplCustom>.postUpdate(<XFeatureCallImplCustom>)
} ] threw an unhandled Exception: 
java.lang.IllegalStateException: Could not invoke method: org.eclipse.xtext.xbase.lib.ObjectExtensions.operator_plus(java.lang.Object,java.lang.String) on instance: null
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1102) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1060) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1046) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:991) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:143) [137:org.eclipse.smarthome.model.script:0.10.0.oh230]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:901) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:225) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) [137:org.eclipse.smarthome.model.script:0.10.0.oh230]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:826) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:263) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) [137:org.eclipse.smarthome.model.script:0.10.0.oh230]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) [137:org.eclipse.smarthome.model.script:0.10.0.oh230]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:189) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) [156:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at com.sun.proxy.$Proxy140.apply(Unknown Source) [?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) [137:org.eclipse.smarthome.model.script:0.10.0.oh230]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh230]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh230]
Caused by: java.lang.IllegalArgumentException: argument type mismatch
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1085) ~[?:?]
	... 23 more
2018-09-28 10:49:47.277 [ERROR] [org.quartz.core.ErrorLogger         ] - Job (DEFAULT.2018-09-28T10:49:47.272+02:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  logInfo(<XStringLiteralImpl>,<XBinaryOperationImplCustom>)
  val newUpdate
  logInfo(<XStringLiteralImpl>,<XBinaryOperationImplCustom>)
  <XFeatureCallImplCustom>.postUpdate(<XFeatureCallImplCustom>)
} ] threw an exception.
org.quartz.SchedulerException: Job threw an unhandled exception.
	at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh230]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh230]
Caused by: java.lang.IllegalStateException: Could not invoke method: org.eclipse.xtext.xbase.lib.ObjectExtensions.operator_plus(java.lang.Object,java.lang.String) on instance: null
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1102) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1060) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1046) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:991) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:143) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:901) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:225) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:826) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:263) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:189) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy140.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
	... 1 more
Caused by: java.lang.IllegalArgumentException: argument type mismatch
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1085) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1060) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1046) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:991) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:143) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:901) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:225) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:826) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:263) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:189) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy140.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
	... 1 more

So I suppose the error does not have much in common with Timers, it’s just some wrong way of using of types, casts or whatever, but I could not figure it out myself.
I also post this to ask whether it’s ok to use it like this. Of course, in the real environment the timers would be longer, but the structure will remain.

You define newUpdate as a val which means constant and your can’t change a constant value that’s why java has a hissy fit.
Define as a var (variable) and you will be able to change its value in the code

Do you use VS code?

Well, I tried this:

var newUpdate = SinceLastUpdate.state + 5

and the error was the same. But then I add

var newUpdate = SinceLastUpdate.state as DecimalType + 5

and it works! I am still a little bit confused when it’s needed to use “as” and when not.
Anyway, thanks. What about this design in general? Do you find it ok, or “according to design patterns”? Or is there a better way of how to achieve what I want.

Hmm, now I need somehow to have 4 timers associated with 4 items. I have 4 items defined like this:

Number 	  Bedroom_SinceLastUpdate         (Bedroom, gLastUpdate)
Number 	  LivingRoom_SinceLastUpdate      (LivingRoom, gLastUpdate) 
Number 	  KidsRoom_SinceLastUpdate	      (KidsRoom, gLastUpdate)
Number 	  Bathroom_SinceLastUpdate	      (Bathroom, gLastUpdate)
Group:Number:MAX       gLastUpdate   "Last update"      (Home)

and a rule:

rule Update
when 
	Member of gLastUpdate received update
then
	logInfo("Debug",triggeringItem + " received update: " + triggeringItem.state)
	
	myTimer = createTimer(now.plusSeconds(5), [ |
		logInfo("Debug","Timer ticked " + triggeringItem.state)
		var newUpdate = triggeringItem.state as DecimalType + 5
		logInfo("Debug","new update " + newUpdate)
		triggeringItem.postUpdate(newUpdate)
	])	
end

but they share one instance of Timer. Can I make an array of 4 Timers AND be able to use appropriate times within the rule above? Or do I need to have 4 timers and 4 rules each for one room?

Replace decimalType with Number
It’s more versatile.

I have two rules of thumb I use.

  1. Try it without the as and if it doesn’t work the first thing to add is the as. This is the easiest to describe and the easiest to implement but it doesn’t really answer your question, just provides a way to handle it.

  2. When the Rules DSL sees any line of code it tries to figure out what the best of the available types to use is. As it does this it goes from left to right. When you have a line like above though, it gets to SinceLastUpdate.state and up to that point it has no hint yet as to what that type should be, so it chooses its best guess which is probably Object. Now if the Rules DSL were really smart it would then move on to the + 5 and realize that it needs to go back and change to Number. Sadly the Rules DSL doesn’t appear to be that smart so it instead throws an error indicating that the type is wrong.

But sometimes it is smart enough to look ahead. For example, if(SinceLastUpdate.state > 5) will work. I suspect is that because we are dealing with a boolean operator it looks at that operator first. Based on the operator it knows that both sides of the > need to be Comparable. And since DecimalType and 5 are both Comparable it works without the as.

So, in practice, you need to use the as when there is not enough information prior to the call to .state when there is nothing prior to the call that tells the Rules DSL that it needs to use it as a Number (or a String, or whatever).

In practice I find this ambiguity is mainly a problem with Numbers and when building a String using an Item’s state. For example:

val msg = MyItem.state + " is the state!" // generates an error because nothing tells the Rules DSL up to the call to .state that it needs to be a String
val msg = MyItem.name + " is set to " + MyItem.state // works because MyItem.name is a String so the Rules DSL knows that we are building a String

The DPs are neither complete nor comprehensive. Just because there isn’t a DP for it yet doesn’t mean it is not a good approach. Your code looks short and concise so I wouldn’t worry too much about the DPs for this.

However…

This is where a couple of the DPs could be useful. See

Thank you for that reply. Concerning the casting (using “as”) am I correct when I assume that

val some_value = 5 + SomeNumberItem.state

would work with the casting? The Rules DSL should recognize it is a number I am building and thus cast it automatically to Number?

And concerning the array of timers, I spent yesterday 4 hours trying to figure it out. Today it finally does what I wanted to do. However, it seems to me that sometimes even if I cancel some timer (and immediatelly recreate another for the same variable), the old time period is kept and thus the timer finishes earlier. In my example (see below) the timer for each room ticks every 5 second and increases the value of _SinceLastUpdate. But if I update the value right before (cca 1-2 secs) the timer expires, the value resets to 0 and in about 1 sec it updates to 5 (like it already past 5 seconds). I enclose my full rules files

import java.util.HashMap

var HashMap<String, Timer> timers = newHashMap("Bedroom" -> null as Timer,
						 "LivingRoom" -> null as Timer,
						 "KidsRoom" -> null as Timer,
						 "Bathroom" -> null as Timer) as HashMap

rule Startup
when
    System started
then
	logInfo("Debug","System started")
end

rule Temperature_changed
when
    Member of gTemperature received update or
	Member of gSetTemperature received update
then
	val roomName = triggeringItem.name.split('_').get(0)
	val heating = gHeating.members.findFirst[t | t.name == roomName+"_Heating"] as SwitchItem
	val temp = gTemperature.members.findFirst[t | t.name == roomName+"_Temperature"] as NumberItem
	val setTemp = gSetTemperature.members.findFirst[t | t.name == roomName+"_Set_Temperature"] as NumberItem
	val lastUpdate = gLastUpdate.members.findFirst[t | t.name == roomName+"_SinceLastUpdate"] as NumberItem
	
	// room received update so let's cancel the already running timer
	if (timers.get(roomName) !== null){
	  logInfo("Debug", "Canceling timer " + roomName)
	  timers.get(roomName).cancel()	
	  timers.put(roomName, null)
	}
	
	if (temp.state < setTemp.state)
		heating.postUpdate(ON)
	if (temp.state > setTemp.state)
		heating.postUpdate(OFF)
	lastUpdate.postUpdate(0)
end

rule Update
when 
	Member of gLastUpdate received update
then
	logInfo("Debug",triggeringItem + " received update: " + triggeringItem.state)
	val roomName = triggeringItem.name.split('_').get(0)
	var Timer timer = timers.get(roomName)
	
	if (timer === null){	// timer didn't run yet so let's put it in the hashmap
		logInfo("Update","Timer " + roomName + " is null, creating new instance")
		timer = createTimer(now.plusSeconds(5), [ |
			triggeringItem.postUpdate(triggeringItem.state as Number + 5)
		])	
		timers.put(roomName, timer)
	} else {	// just restart the timer. I cannot use reschedule because I need to update the state value too
		logInfo("Update","Timer " + roomName + " is already ticking, restarting the timer with +5 seconds")
		timer.cancel()
		Thread::sleep(500)
		timer = createTimer(now.plusSeconds(4.5), [ |
			triggeringItem.postUpdate(triggeringItem.state as Number + 5)
		])	
	}
end

I’ve had it work in the past.

There is absolutely no reason to populate a HashMap with nulls. Setting the value associated with a key to null removes that key/value pair from the map. So all that long line you have that populates the Map is exactly equivalent to

val Map<String, Timer> timers = newHashMap

Note, it is usually best practice to use the more generic “Map” instead of the more specific HashMap, though that probably doesn’t matter here.

You can simplify the canceling line a little using

timers.get(roomName)?.cancel
timers.put(roomName, null)

I don’t think you can use decimals with now.plusSeconds. If you want 4.5 seconds you need to use now.plusMillis(4500).

Why are you sleeping before creating the timer?

You never put the Timer you create in the else clause back into the Map which is probably the source of the problem.

I have resolved the issue in the meantime (yes, it was the last line you wrote). I sort of didn’t imagine what is happening during each call (map.get, map.put, timer = createTimer() etc.). So now it works fine:

rule Temperature_update
when
    Member of gTemperature received update
then
	val roomName = triggeringItem.name.split('_').get(0)
	val lastUpdate = gLastUpdate.members.findFirst[t | t.name == roomName+"_SinceLastUpdate"] as NumberItem
	lastUpdate.postUpdate(0)
end

rule Sensor_Update
when 
	Member of gLastUpdate received update
then
	logInfo("Update",triggeringItem + " received update: " + triggeringItem.state)
	val roomName = triggeringItem.name.split('_').get(0)
	var Timer timer = timers.get(roomName)
	
	if (timer !== null){	// timer is already running, so cancel it
		logInfo("Update","Timer " + roomName + " is already ticking, restarting the timer with +" + TIMER_PERIOD + " seconds")
		timer.cancel()
	} else {
		logInfo("Update","Timer " + roomName + " is null, creating new instance")
	}
	timer = createTimer(now.plusSeconds(TIMER_PERIOD), [ |
		triggeringItem.postUpdate(triggeringItem.state as Number + TIMER_PERIOD)
	])	
	timers.put(roomName, timer)
end

The timers HashMap I saw somewhere here, in the forums, so I just copied it. But you are right, simply empty Map would serve the same :slight_smile: I will adjust that. Thanks