An Approach for Better Light Timers

edit: I’ve updated the code to show the final form of what I have in my rules file, having to work around limitations of the language caused it to diverge from my original idea, but it works well and makes the rest of my rules far more maintainable and readable.

I have been annoyed with how setting up timers for turning off lights is done in OH. Mainly for two reasons;
having two different triggers in different .rules files (front porch lights on door open and also when a new Presence is detected), the timers can step on each other and if you ever edit a file, the timers that are saved as global variables in the .rules files are lost and the logs are filled with errors once they expire.

I’m experimenting with a new timeout pattern and thought I would share it for discussion. The approach is this:

Create new items to match each of your Switch and Dimmer items, prepending Timer_ to the name.

Group gLightTimers

Number Timer_SwitchFrontFloods_Switch					(gLightTimers)	// matches SwitchFrontFloods_Switch
Number Timer_SwitchDownstairsHall_Dimmer 				(gLightTimers)	// matches SwitchDownstairsHall_Dimmer
...

create a new .rules file and add a big old ugly “function” at the top:

import java.util.HashMap

var HashMap<String, Pair<DateTime, Timer>> SwitchTimers = newHashMap()


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
val Functions$Function2<GenericItem, HashMap<String, Pair<DateTime, Timer>>, DateTime> doTimer=[GenericItem lightTimeoutItem, HashMap<String, Pair<DateTime, Timer>> SwitchTimers |

	val DateTime lightTimeout = new DateTime((lightTimeoutItem.state as DateTimeType).calendar.timeInMillis)
	val String switchName = lightTimeoutItem.name.substring(6)
	val Pair<DateTime, Timer> existingEntry = SwitchTimers.get(switchName)
	var Timer currentTimer = null
	var DateTime currtimeoutTime
	
		// if there is an existing timer item, load its values if it is still valid
	if(existingEntry != null)
	{
		currtimeoutTime = existingEntry.key
		if(currtimeoutTime.isAfter(now))
		{
			currentTimer = existingEntry.value		
		}
	}
	
		// if the timer item is for the future
	if(lightTimeout.isAfter(now))
	{
		val lightSwitch = gLights.allMembers.findFirst[name.equals(switchName)]

		// make sure that it is valid to turn the light on and set a new timer (if there isn't a valid current timer or this timer is later than the previous one, or the light is off)
		if(currentTimer == null || currentTimer.hasTerminated || lightTimeout.isAfter(currtimeoutTime) || lightSwitch.state == OFF || lightSwitch.state == 0)
		{
			// cheat here, pull the dimmer value out of the timer millisecond values
			var dimVal = lightTimeout.getMillisOfSecond()

			// cancel any current timer
			if(currentTimer != null && !currentTimer.hasTerminated)
			{
				currentTimer.cancel()
			}	
			
			// turn on the light if the dimVal is within an acceptable range
			if(switchName.contains("_Dimmer"))
			{
				if(dimVal > 0 && dimVal < 100)
				{
					logInfo("Illumination", "Turning on " + switchName + " to " + dimVal + "%")
					lightSwitch.sendCommand(dimVal)
				}
			}
			else
			{
				// if dimVal is 0, don't actually turn on the light
				if(dimVal > 0)
				{
					logInfo("Illumination", "Turning on " + switchName)
					lightSwitch.sendCommand(ON)
				}
			}
			
			// create a new timer and load it into the hashmap
				// have to replace the millisOfSecond with the actual value, as creating two timers at the exact same time actually causes a problem with the framework
			SwitchTimers.put(switchName, new Pair(lightTimeout, createTimer(lightTimeout.withMillisOfSecond(now.getMillisOfSecond())) [|
				logInfo("Illumination", "Timer Turning off " + switchName)
				lightSwitch.sendCommand(OFF)
				]))
		}
	}
	else if(existingEntry != null && currentTimer != null && !currentTimer.hasTerminated)
	{
		// if the value is in the past, just kill the current timer
		currentTimer.cancel()
	}
	
	return lightTimeout
]

then, unfortunately, create a rule for every Timer_ item:

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rule "Light Timeout Timer_SwitchFrontFloods_Switch"
when
	Item Timer_SwitchFrontFloods_Switch changed                                                                            
then
	doTimer.apply(Timer_SwitchFrontFloods_Switch, SwitchTimers)
end 
  
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rule "Light Timeout Timer_SwitchDownstairsHall_Dimmer"
when
	Item Timer_SwitchDownstairsHall_Dimmer changed                                                                            
then
	doTimer.apply(Timer_SwitchDownstairsHall_Dimmer, SwitchTimers)
end  

//etc, etc ( I have 35 light switches, so its a lot of copy/paste)

now, wherever you used to check if a timer exists, then cancel it, then send a command to an item, then create a timer with a closure to turn it off after a set time, you can replace with this:

Timer_SwitchFrontFloods_Switch.postUpdate(new DateTimeType(now.plusMinutes(15)).toString)
or for dimmers (this is slightly messy as I want to send the level instead of just assuming OFF/ON, so I use the millisOfSecond value to store the dimmer level so the actual timeout will be +- 1second). You could use a second item for this as well.
Timer_SwitchDownstairsHall_Dimmer.postUpdate(new DateTimeType((now.plusMinutes(15).withMillisOfSecond(50)).toString)) // sets the level to 50
you can also cancel an existing timer (useful for motion sensor rules) by updating the timer to a value in the past
Timer_SwitchFrontFloods_Switch.postUpdate(new DateTimeType(now.minusMinutes(1)).toString)

Finally, I’ve found in my motion rules that being able to update the timer without turning the light on is useful so if the level is 0 for either a dimmer or switch item, the timer will be created but the light won’t be turned on

Timer_SwitchDownstairsHall_Dimmer.postUpdate(new DateTimeType((now.plusMinutes(15).withMillisOfSecond(0)).toString)) 

This code will track the timeouts in a central place so there is no more conflict between timers. It also ensures that if multiple commands come in for a single light switch, the latest timeout will be used. If you never touch this .rules file, then you can edit the rest of your rules without breaking all the running timers.

It would be ideal if a method that encompasses all this could be integrated into the framework, something like:
sendCommandWithTimeout(, state, timeout, timeoutstate)

1 Like

I would use the Expire binding and Associated Items (which you already are using).

  • Still create your new Item with “Timer_” appended to the name. Make these Item Switches and bind these Items to the Expire binding.
  • Add your light Swtiches and Dimmers to the same group, I’ll assume it is gLights below.
Group gLightTimers

Switch Timer_SwitchFrontFloods_Switch    (gLightTimers) { expire="5m,command=OFF" }
Switch Timer_SwitchDownstairsHall_Dimmer (gLightTimers) { expire="10m,command=OFF" }

The rules become very simple:

rule "gLights received update"
when
    Item SwitchFrontFloods_Switch received command ON or
    Item SwitchDownstairsHall_Dimmer received command ON
then
    Thread::sleep(100) // may be required for persistence to catch up
    val light = gLights.members.filter[l | l.lastUpdate != null].sortBy[lastUpdate].last
    val timer = gLightsTimers.members.filter[t | t.name == "Timer_"+light.name].head
    timer.sendCommand(ON)
end

rule "Timer went OFF"
when
    Item Timer_SwitchFrontFloods_Switch received command OFF or
    Item Timer_SwitchDownstairsHall_Dimmer received command OFF
then 
    Thread::sleep(100) // may be required for persistence to catch up
    val timer = gLightsTimers.members.filter[t | t.lastUpdate != null].sortBy[lastUpdate].last
    val light = val light = gLights.members.filter[l | "Timer_"+l.name == timer.name].sortBy[lastUpdate].last
    light.sendCommand(OFF)
end

If these lights will always be timer based (i.e. there is no way/desire to turn them on and have them remain ON) you don’t even need the rules. Just add the Expire binding to the Light Switch and Dimmer Items directly.

The only limitation is the timeout is hardcoded rather than based on an Item you can change on your sitemap.

Check and check. These rules do that too.

You can change the .rules file all you want with this configuration. But if you change the .items files your timers will be wiped out.

A better approach would be to use the Joda DateTimes anyway. Store now.plusMinutes(15) as your timeoutTime then to check you see if(now.isAfter(timeoutTime)) or just pass `timeoutTime to the timer directly:

createTimer(now.plusMinutes(15), [| ...

This isn’t Java. Rules are a Domain Specific Language written on top of the Xtext language which runs inside of Java. You have access to some Java libraries but the syntax is decidely not Java.

1 Like

There is the rub, I have different timeouts based on occupancy, sun state and occupant sleep state.

working on this, but I’d still like to encode the dimmer level in the value so I’m fiddling with changing the millisOfSecond field.

1 Like

This isn’t some embedded program running under extreme resource constraints. Personally, I value my time more than the computer’s. Just create another Item for the dimmer level.

In that case, I would code it something like this:

  • Same Items you have
  • Add Dimmer Items for the desired dimmer level, prepend the name with “DimTo_” or something else you find appropriate, add these to a gDimTo group
import java.util.HashMap

val HashMap<String, Timer> timers = newHashMap()

rule "Lights Timeouts"
when
    Item gLightTimers received update
then
    val timerItem = gLightsTimers.members.filter[c|c.lastUpdate != null].sortBy[lastUpdate].head
    val timerTime = new DateTime(timerItem.state as Number) // timerItem.state is epoc
    val lightName = timerItem.name.replace("Timer_", "")
    val light= gLights.allMembers.findFirst[name == switchName] as GenericItem

    // make sure the timer time is valid
    if(timerTime.isAfter(now)){

        // there are no running timers
        if(timers.get(lightName) == null){ 

            // turn on light
            if(light instanceof SwitchItem){
                light.sendCommand(ON)
            }
            else {
                val dimmerValue = gDimTo.members.findFirst[name == "DimTo_"+lightName] as DimmerItem
                light.sendCommand(dimmerValue.state as PercentType)
            }

            // create a Timer to turn it off
            timers.put(lightName, createTimer(timerItem, [|
                light.sendCommand(OFF)
                timers.put(lightName, null)
            ]))
        }

        // reschedule existing timer 
        else {
            timers.get(switchName).reschedule(timerTime)
        }
    }
    else {
        logError("Illumination", "Cannot set timer for " + switchName + " timer is scheduled for the past")
    }
end

// example of how to trigger the above rule
rule "Some Timer Trigger"
when
    Item SomeMotionSensor received update
then
    // calculation for how long to timeout
    var timeoutMins = if(TimeOfDay.state == "NIGHT") now.plusMinutes(15).millis else now.plusMinutes(5).millis

    // calculate dimmer value
    val dimmerPercent = if(TimeOfDay.state == "NIGHT") 50 else 100

    DimTo_SwitchDownstairsHall_Dimmer.sendCommand(dimmerPercent)
    Timer_SwitchDownstairsHall_Dimmer.sendCommand(now.plusMinutes(timeoutMins))
end

Theory of operation. The calling rule (i.e. the one triggered by the motion sensor et al) calculates the amount of time for the light to stay on and the dimmer value for the light (if appropriate). The timer rule triggers, turns on the light (or sets the dimmer value if it is a dimmer) and creates a Timer based on the value of the Timer_ Item. If a Timer already exists the existing Timer is rescheduled.

If you are not paranoid you could probably do away with what little error checking I have above, or add more if that suits you better.

Note I just typed this in. It likely contains typos and other mistakes.

One good alternative/change would be to use a DateTime Item instead of a Number Item for the Timers. It would require some more conversions though so I would only look into doing that if you planned on putting these Timer times on your sitemap.

I thought about that, but I went the other way just to make it so my rules have a single line per command.

trying to do that but I’ve searched for the answer to using DateTimeItems but I’m still not sure how to make this work.

I can postUpdate to a DateTimeItem by doing:

	Timer_SwitchDownstairsHall_Dimmer.postUpdate(new DateTimeType(now.plusMinutes(1).toString))

but in the rule if I call:

	val lightTimeoutItem = gLightTimers.members.filter[c|c.lastUpdate != null].sortBy[lastUpdate].last
	val DateTime lightTimeout = lightTimeoutItem.state as DateTimeType
	var test = lightTimeout.getMillisOfSecond()

I get:

15:17:28.972 [ERROR] [.script.engine.ScriptExecutionThread] - Rule 'Light Timeouts': An error occured during the script execution: Could not invoke method: org.joda.time.base.AbstractDateTime.getMillisOfSecond() on instance: 2017-03-13T15:18:26.050-0700
1

the calls that use DateTime objects, like now.isAfter() also fail, I’m not sure what is going on here,

Seems like an awful lot of extra work to me but to each his own.

The problem is that DateTimeType isn’t a Joda DateTime object. The two are not compatible. It’s a real pain and the main reason I didn’t go down that route in the first place.

What you want to do in your rule is:

val DateTime lightTimeout = new DateTime((lightTimeoutItem.state as DateTimeType).calendar.timeInMillis)

ahhh, that worked.

for me, since I have a lot of rules that do this, I’d rather have the one item and one command.

though it needs to check if the already scheduled timer should be rescheduled, if the new command is 5 minutes but something else scheduled 60 minutes, I don’t want to reschedule the timer for 5. I’ll update the code in the OP with what I have now with the datetime.

of course, I ran into the problem that kills this idea. trying to set 2 lights in a row in a rule causes the dreaded

"Rule 'Light Timeouts': null" 

error that happens when you try to do the lastUpdate trick but items update too close together. Maybe I’ll just use your original suggestion and sacrifice my fancy context aware timeouts and just use the expire binding.

Ah. I missed that. I was trying to figure out what that Pair Integer was all about. Ok, here is what I would do for that.

First, make sure the Timer_ Items are persisted, and not in a MapDB as we need to get at the historic values. Then (assuming you switched over to using DateTime Items replace if(timerTime.isAfter(now)){ with:

val lastTimerValue = new DateTime((timerItem.previousState(true).state as DateTimeType).calendar.millis)
if(timerTime.isAfter(now) && timerTime.isAfter(lastTimerValue){
    ...
}
else {
    timerItem.postUpdate(lastTimerValue.toString) // change the timer value back to the old value
}

This pulls that previous different value of the timer and only if the new timer value is later will it execute the code. If it isn’t we set the Timer back to the previous value.

That should be solvable with a Thread::sleep(100) or so. you may need to experiment with the millis to find a pause that gives persistence a chance to catch up. I don’t think that problem is unsolvable.

While this is a complicated use case, I do not think it is a unique one. I’d really like to see this work.

So the full rule becomes:

import java.util.HashMap

val HashMap<String, Timer> timers = newHashMap()

rule "Lights Timeouts"
when
    Item gLightTimers received update
then
    val timerItem = gLightsTimers.members.filter[c|c.lastUpdate != null].sortBy[lastUpdate].head
    val timerTime = new DateTime(timerItem.state as Number) // timerItem.state is epoc
    val lastTimerTime = new DateTime((timerItem.previousState(true).state as DateTimeType).calendar.millis)
    val lightName = timerItem.name.replace("Timer_", "")
    val light= gLights.allMembers.findFirst[name == switchName] as GenericItem

    // make sure the timer time is valid
    if(timerTime.isAfter(now) && timerTime.isAfter(lastTimerTime)){

        // there are no running timers
        if(timers.get(lightName) == null){ 

            // turn on light
            if(light instanceof SwitchItem){
                light.sendCommand(ON)
            }
            else {
                val dimmerValue = gDimTo.members.findFirst[name == "DimTo_"+lightName] as DimmerItem
                light.sendCommand(dimmerValue.state as PercentType)
            }

            // create a Timer to turn it off
            timers.put(lightName, createTimer(timerItem, [|
                light.sendCommand(OFF)
                timers.put(lightName, null)
            ]))
        }

        // reschedule existing timer 
        else {
            timers.get(switchName).reschedule(timerTime)
        }
    }
    else {
        logError("Illumination", "Cannot set timer for " + switchName + " timer is scheduled for the past or earlier than the current Timer")
        timerItem.postUpdate(lastTimerTime)
    }
end

I think i’ll keep the previous state of the timer in the HashMap as I’ve only got MapDB and rrd4j as persistence services and thats as far as I want to go with that. I gave up on my MySql persistence a while ago, though I did write a handy script that would run daily to wipe all but the last 5 datapoints.

In the rule? I’ve tried several values up to 1000 and still the same error. I experimented with sleeping in different places in the rule and all I got was the rule firing twice on the same item (as it was the last updated). I tried putting the sleep between the two items sendCommands in my test rule but the rules are synchronous so it just delayed the execution of the handler rule

I believe you can store DateTimeType in rrd4j. You used to be able to and I’ve seen no mention in these forums that that has changed.

I misunderstood what the problem was. It isn’t sending two commands too close together, it is multiple triggers of the rule so there are two or more running at the same time and trying to sendCommand.

So there are two ways to solve this. An easy way and a harder way. The easy way is to trigger the Rule on each Item individually rather than the Group. The Groups get updated multiple times for each change to one of its members.

The harder way is to add a reentrant lock so only one instance of the Rule can run at a time. Though that may run the risk of messing up your ability to pull the latest updated Item from the Group.

It doesn’t look like it, I check the persistence REST API call for testing and get no data points.

I’ll try changing the rule to a large list of “item xx received update or” and see if that helps.

Keep in mind nothing will be added to the store until they actually have an update or change. So make sure to set the Timer Items to something after configuring them to save in rrd4j but before you check in the REST API (or however else you want to check for them).

I can get the first to work if I make sure to put a 500ms sleep between postUpdate() calls but I really don’t like timing dependencies. I think my best bet is to make 30 individual item rules that call a lambda to execute the common code for each item. This is at least not dependent on the load of the system.

Kinda wish there were a reliable way to actually know what item triggered a rule so I could just have one rule with 30 triggers.

I’ve updated the OP with what I’ve ended up with. It kinda got away from me but I’m happy with the functionality at this point and it has cleaned up the rest of my rules so they are more readable and manageable.

I added the ability to simply cancel the timer to count for when motion starts, I want to cancel the timer and turn the light on indefinitely (or other usages in my rules, like cancelling all light timers in case of a smoke alarm so I can turn all the lights on and they won’t randomly go off)

I also added the ability to create a timer without turning a light on, so when the motion stops in a room, the timer is created (but if the person turned the light off before they left, it won’t turn it back on).

Hi there, I decided to follow your instructions and did the following

import java.util.HashMap
import org.joda.time.DateTime

var HashMap<String, Pair<DateTime, Timer>> SwitchTimers = newHashMap()


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
val Functions$Function2<GenericItem, HashMap<String, Pair<DateTime, Timer>>, DateTime> doTimer=[GenericItem lightTimeoutItem, HashMap<String, Pair<DateTime, Timer>> SwitchTimers |

	val DateTime lightTimeout = new DateTime((lightTimeoutItem.state as DateTimeType).calendar.timeInMillis)
	val String switchName = lightTimeoutItem.name.substring(6)
	val Pair<DateTime, Timer> existingEntry = SwitchTimers.get(switchName)
	var Timer currentTimer = null
	var DateTime currtimeoutTime
	
		// if there is an existing timer item, load its values if it is still valid
	if(existingEntry != null)
	{
		currtimeoutTime = existingEntry.key
		if(currtimeoutTime.isAfter(now))
		{
			currentTimer = existingEntry.value		
		}
	}
	
		// if the timer item is for the future
	if(lightTimeout.isAfter(now))
	{
		val lightSwitch = group____LIGHT.allMembers.findFirst[name.equals(switchName)]

		// make sure that it is valid to turn the light on and set a new timer (if there isn't a valid current timer or this timer is later than the previous one, or the light is off)
		if(currentTimer == null || currentTimer.hasTerminated || lightTimeout.isAfter(currtimeoutTime) || lightSwitch.state == OFF || lightSwitch.state == 0)
		{
			// cheat here, pull the dimmer value out of the timer millisecond values
			var dimVal = lightTimeout.getMillisOfSecond()

			// cancel any current timer
			if(currentTimer != null && !currentTimer.hasTerminated)
			{
				currentTimer.cancel()
			}	
			
			// turn on the light if the dimVal is within an acceptable range
			if(switchName.contains("_Dimmer"))
			{
				if(dimVal > 0 && dimVal < 100)
				{
					logInfo("Illumination", "Turning on " + switchName + " to " + dimVal + "%")
					lightSwitch.sendCommand(dimVal)
				}
			}
			else
			{
				// if dimVal is 0, don't actually turn on the light
				if(dimVal > 0)
				{
					logInfo("Illumination", "Turning on " + switchName)
					lightSwitch.sendCommand(ON)
				}
			}
			
			// create a new timer and load it into the hashmap
				// have to replace the millisOfSecond with the actual value, as creating two timers at the exact same time actually causes a problem with the framework
			SwitchTimers.put(switchName, new Pair(lightTimeout, createTimer(lightTimeout.withMillisOfSecond(now.getMillisOfSecond())) [|
				logInfo("Illumination", "Timer Turning off " + switchName)
				lightSwitch.sendCommand(OFF)
				]))
		}
	}
	else if(existingEntry != null && currentTimer != null && !currentTimer.hasTerminated)
	{
		// if the value is in the past, just kill the current timer
		currentTimer.cancel()
	}
	
	return lightTimeout
]





rule "LIGHT-TIMER"
when
Item group____LIGHT received update
//Item disable_LightTimers changed

then

	val lightValue = illumination_sensor_aussendachmastlicht.state
	
	val mostRecent = group____LIGHT?.allMembers.sortBy[lastUpdate].last
	val timeItemName = "Timer_" + mostRecent.name
	val timerItem = gLightTimers.allMembers.findFirst[name.equals(timeItemName)]
	logInfo("Powersaving", "Most recent"+mostRecent.name)
	logInfo("Powersaving", "lightValue: "+lightValue)
	timerItem.postUpdate(new DateTimeType(now.plusMinutes(15)).toString)
end








	rule "Light Timeout Timer_switchlight_kellerwerkstattdeckenlampe"
	when
		Item Timer_switchlight_kellerwerkstattdeckenlampe changed                                                                            
	then
		doTimer.apply(Timer_switchlight_kellerwerkstattdeckenlampe, SwitchTimers)
	end 
	rule "Light Timeout Timer_switchlight_kelleraufbewahrungsraumdeckenlampe"
	when
		Item Timer_switchlight_kelleraufbewahrungsraumdeckenlampe changed                                                                            
	then
		doTimer.apply(Timer_switchlight_kelleraufbewahrungsraumdeckenlampe, SwitchTimers)
	end 
  ...

Now I´m running into troubles with it.

  1. It won´t compile. This code
timerItem.postUpdate(new DateTimeType(now.plusMinutes(15)).toString)

brings up this
image

Did I understand something wrong ?

you’ve got a paren wrong there:

it should be now.plusMinutes(15).toString as the argument for new DateTimeType()

so it should be :

timerItem.postUpdate(new DateTimeType(now.plusMinutes(15).toString))

ain’t XTend a kick in the balls :slight_smile:

It doesn´t work. Probably I´m just too dumb for this wor … ah …let´s say at least for openhab :slight_smile:

Again … this is my code

import java.util.HashMap
import org.joda.time.DateTime
import java.util.Locale

var HashMap<String, Pair<DateTime, Timer>> SwitchTimers = newHashMap()


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
val Functions$Function2<GenericItem, HashMap<String, Pair<DateTime, Timer>>, DateTime> doTimer=[GenericItem lightTimeoutItem, HashMap<String, Pair<DateTime, Timer>> SwitchTimers |

	val DateTime lightTimeout = new DateTime((lightTimeoutItem.state as DateTimeType).calendar.timeInMillis)
	val String switchName = lightTimeoutItem.name.substring(6)
	val Pair<DateTime, Timer> existingEntry = SwitchTimers.get(switchName)
	var Timer currentTimer = null
	var DateTime currtimeoutTime
	
		// if there is an existing timer item, load its values if it is still valid
	if(existingEntry != null)
	{
		currtimeoutTime = existingEntry.key
		if(currtimeoutTime.isAfter(now))
		{
			currentTimer = existingEntry.value		
		}
	}
	
		// if the timer item is for the future
	if(lightTimeout.isAfter(now))
	{
		val lightSwitch = group____LIGHT.allMembers.findFirst[name.equals(switchName)]

		// make sure that it is valid to turn the light on and set a new timer (if there isn't a valid current timer or this timer is later than the previous one, or the light is off)
		if(currentTimer == null || currentTimer.hasTerminated || lightTimeout.isAfter(currtimeoutTime) || lightSwitch.state == OFF || lightSwitch.state == 0)
		{
			// cheat here, pull the dimmer value out of the timer millisecond values
			var dimVal = lightTimeout.getMillisOfSecond()

			// cancel any current timer
			if(currentTimer != null && !currentTimer.hasTerminated)
			{
				currentTimer.cancel()
			}	
			
			// turn on the light if the dimVal is within an acceptable range
			if(switchName.contains("_Dimmer"))
			{
				if(dimVal > 0 && dimVal < 100)
				{
					logInfo("Illumination", "Turning on " + switchName + " to " + dimVal + "%")
					lightSwitch.sendCommand(dimVal)
				}
			}
			else
			{
				// if dimVal is 0, don't actually turn on the light
				if(dimVal > 0)
				{
					logInfo("Illumination", "Turning on " + switchName)
					lightSwitch.sendCommand(ON)
				}
			}
			
			// create a new timer and load it into the hashmap
				// have to replace the millisOfSecond with the actual value, as creating two timers at the exact same time actually causes a problem with the framework
			SwitchTimers.put(switchName, new Pair(lightTimeout, createTimer(lightTimeout.withMillisOfSecond(now.getMillisOfSecond())) [|
				logInfo("Illumination", "Timer Turning off " + switchName)
				lightSwitch.sendCommand(OFF)
				]))
		}
	}
	else if(existingEntry != null && currentTimer != null && !currentTimer.hasTerminated)
	{
		// if the value is in the past, just kill the current timer
		currentTimer.cancel()
	}
	
	return lightTimeout
]


//SECTION, WHICH IS SUPPOSED TO TRIGGER THE TIMERS AFTER A LIGHT HAS BEEN SWITCHED ON


rule "LIGHT-TIMER"
when
Item group____LIGHT received update
//Item disable_LightTimers changed

then

	val lightValue = illumination_sensor_aussendachmastlicht.state
	
	val mostRecent = group____LIGHT?.allMembers.sortBy[lastUpdate].last
	val timerItemName = "Timer_" + mostRecent.name
	val timerItem = gLightTimers.allMembers.findFirst[name.equals(timerItemName)]
	logInfo("Powersaving", "NEW LIGHTSAVING ("+timerItemName+")")
	logInfo("Powersaving", "Most recent"+mostRecent.name)
	logInfo("Powersaving", "lightValue: "+lightValue)
	//timerItem.postUpdate(new DateTimeType(now.plusMinutes(15).toCalendar(Locale.GERMAN)).toString)
	timerItem.postUpdate(new DateTimeType(now.plusSeconds(15).toString))
end




//INDIVIDUAL RULES FOR EVERY LIGHT



	rule "Light Timeout Timer_switchlight_kellerwerkstattdeckenlampe"
	when
		Item Timer_switchlight_kellerwerkstattdeckenlampe changed                                                                            
	then
		doTimer.apply(Timer_switchlight_kellerwerkstattdeckenlampe, SwitchTimers)
	end 
......

Nothing happens, and debugging is a hell here :frowning:

So the first thing to do is add logging to verify that your rules are being triggered. If not you know your Rule triggers are not working or your Items are not receiving the updates as expected.

Next, add some logging to your rules and lambda to log out the information on each step of the way.

If you are using a very recent SNAPSHOT version of OH I recommend using the VSCode with openHAB addon to help identify syntax errors like the missed parens from above. I think the language server feature is ready for use in the latest snapshots. If not, I recommend using Eclipse Smarthome Designer which, while not completely up to date (it will mark some new features of OH rules, Items defined in PaperUI, and non-default Actions as errors) but it will also help identify basic syntax problems.

If you are running OH 1.8, then your only option is openHAB Designer.

Anyway, if the problem is still the same line:

You can just bypass the problem by leaving lightTimeout as a long.

val long lightTimeout = (lightTimeoutItem.state as DateTimeType).calendar.timeInMillis

then change your comparison later on to

if(now.isBefore(lightTimeout))

or

if(lightTimeout  > now.millis)

and you can get the millis of the second with

var dimVal = lightTimeout % 1000