Tutorial: How I make timers that show remaining execution time

Tags: #<Tag:0x00007f61737d7e28> #<Tag:0x00007f61737d7c48>

Background story
I once used expire binding to create auto off lamps, but 1 switch in a 2 gang switch is not expiring properly similar with this thread. I also like to know how long my switches will be turned off to save electricity, so I need to know remaining time of timer. After receiving many help on this wonderful forum, I want to share what I do to achieve without learning another language (I have no Java nor Python background).

With fear of expire binding go away in OH 3, and my limited understanding on python to use this python based solution to show timer remaining time, I decided to create my way to show timer remaining time using proxy item using design pattern: associated items.

How I do it
sample of items.items

Group	Group_Counters_Triggers		<time>	(Group_Counters)		
Group	Group_Counters_Minutes		<time>	(Group_Counters)		
Group	Group_Counters_Seconds		<time>	(Group_Counters)

Switch	Kodi1_stop	"Stop"		(Group_Kodi, Group_Counters_Triggers)
Switch	Light_FF_Front	"Front Hall"	<light>	(Group_Counters_Triggers)

Number  Kodi1_stop__Counter "TV Shutdown timer [%d]"  <clock> (Group_Counters_Minutes)    // notice double underscore before "Counter"
Number  Light_FF_Front__Counter "Front Hall timer [%d]"  <clock> (Group_Counters_Seconds)

timer.rules

import org.eclipse.smarthome.model.script.ScriptServiceUtil
var Timer tKill = null
var Timer tKillSeconds = null

rule "Counter Triggers"
when
    Member of Group_Counters_Triggers received command
then
	val itemCounter = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name + "__Counter" )
	switch(triggeringItem.name){
		case "Kodi1_stop": {
			switch triggeringItem.state.toString {
				case "ON": { itemCounter.sendCommand(8)
				}
				case "OFF": { itemCounter.sendCommand(-1) // I use "-1" because I do not know how to stop timer without using value lower than 0
				}
			}
		}
		case "Light_FF_Front": {
			switch triggeringItem.state.toString {
				case "ON": { itemCounter.sendCommand(30)
				}
				case "OFF": { itemCounter.sendCommand(-1)
				}
			}
		}
	}
end


rule "CounterMinutes Commands"
when
    Member of Group_Counters_Minutes received command
then
	Thread::sleep(10) //required to give rule to receive changed state, without this old state will be used
	logWarn("Counter Minutes Action", triggeringItem.state.toString + " = value of " + triggeringItem.name)
	tKill = createTimer(now.plusMinutes(1), [|
	val stopCounter = Integer::parseInt(triggeringItem.state.toString)
	logWarn("CounterMinutes Commands", "Timer " + triggeringItem.name + " runs, value = " + stopCounter)
	if(stopCounter == 0){
		switch(triggeringItem.name){
			case "Kodi1_stop__Counter": {
				if(Kodi1_stop.state == ON){ //only shutdown TV when nothing is playing. I didn't know how to get "paused" state yet.
					TV_FF_Living_Power.sendCommand(OFF)
					logWarn("TV Shutdown Timer", "TV shutted down, Kodi1_stop state = " + Kodi1_stop.state.toString)
				}
				else logWarn("TV Shutdown Timer", "something wrong, Kodi1_stop status = " + Kodi1_stop.state.toString)
			}
			default : { 
				val counterTrigger = ScriptServiceUtil.getItemRegistry.getItem( triggeringItem.name.split('__').get(0) )
				counterTrigger.sendCommand(OFF)
			}
		}
		logWarn("CounterMinutes Commands", "Counter is now " + stopCounter + ", Action for " + triggeringItem.name + " is executed. Then timer is deactivated")
		tKill = null
	}
	if(stopCounter > 0){
		logWarn("CounterMinutes Commands", "counter is not negative, reducing timer..")
		triggeringItem.sendCommand(stopCounter - 1)
	}
	if(stopCounter === null) tKill = null
	])
end


rule "CounterSeconds Commands"
when
    Member of Group_Counters_Seconds received command
then
	Thread::sleep(10)
	logWarn("Counter Seconds Action", triggeringItem.state.toString + " = value of " + triggeringItem.name)
	tKillSeconds = createTimer(now.plusSeconds(1), [|
	val stopCounter = Integer::parseInt(triggeringItem.state.toString)
	if(stopCounter == 0){
		switch(triggeringItem.name){
			default : { 
				val counterTrigger = ScriptServiceUtil.getItemRegistry.getItem( triggeringItem.name.split('__').get(0) )
				counterTrigger.sendCommand(OFF)
			}
		}
		logWarn("Counter Seconds Commands", "Counter is now " + stopCounter + ", Action for " + triggeringItem.name + " is executed. Then timer is deactivated")
		tKillSeconds = null
	}
	if(stopCounter > 0){
		triggeringItem.sendCommand(stopCounter - 1)
	}
	if(stopCounter === null) tKillSeconds = null
	])
end

old.sitemap

sitemap old label="Main Menu old"
{
    Switch item=Light_SF_MasterBedRoom_BathroomLightSwitch1
    Slider item=Light_SF_MasterBedRoom_BathroomLightSwitch1__Counter
    Switch item=TV_FF_Living_Power
    Text item=Kodi1_stop__Counter
}

here is what I get on sitemap
Slider to change timer on demand.

That’s all.
Feedback is highly appreciated.

3 Likes

rossko57 had a design pattern that does this as well using expire. Design Pattern : Expire Binding based Countdown timer There is a link to a Python version that can be just dropped in and used.

I didn’t do a thorough analysis of the code but everything looks good except for what I mention below.

According to github, it looks like the maintainers are working on an expire replacement that will be compatible with the 1.x binding.

Don’t use Thread:sleep. Instead use ‘receivedCommand’. Look in the rules docs implicit variable section for many other helper variables that are available.

Since the Items are Number Items, use triggeringItem.state as Number rather than converting the Number to a String only to parse it back to a Number.

It is very nice to be able to reduce thread:sleep. Thanks for the pointer of using receivedCommand
Because I now use receivedCommand, triggeringItem.state is no longer needed.