Smart Virtual Thermostat (beta version)

Tags: #<Tag:0x00007f744f5d1860> #<Tag:0x00007f744f5d1798> #<Tag:0x00007f744f5d16a8>

I’ve tried to “translate” the Smart Virtual Thermostat plugin from Domoticz to OpenHAB. My goals is to create a DP tutorial for everyone.

What does it do?
The rule decide based on the temperature of the room and outdoor temperature (to be made) heating duration necessary to achieve or maintain wishes temperature. To do that, once per amount of time (for now 30 minutes) or when setpoint changes, etc, the rule calculates a percentage of heating. As an example (values ​​are fictive) the temperature is 21° and the room temperature to 20.3°, the rule estimate a heat time of 30%. It turns On heaters during the first 10 minutes, then Off for next 20 minutes. After a 30 minutes, it restarts the calculation to estimate the need for next hour and adjusting the constant.
The goal of this is to overcome over- or undershoots. Keeping a room a temperature is (unfortunately) not as simpel as:

if (temp<setpoint) {
  boiler.sendCommand(ON)
} else {
  boiler.sendCommand(OFF)
}

Because each room or area has their own characteristics: thermal inertia – heating power – outside Insulation, heating time required to reach the setpoint is specific to each of them.
The rule is learning to calculate right heating time and thus do not consume more than necessary. At each end of the heating phase, it examines and adjusts coefficients.
It’ll take a few days before it works as it suppose to be. It has to “learn” the room or area

But for now
It needs testing en polishing the code, so please try it and give me feedback.
There are a few features not included (yet):

  • Constant calculated based on outside temperature (ConstT)
  • Minimal heating per cycle
  • Pause mode
  • If the items are new/first time use (=NULL) then fill them with defaults

All the items and groups that suppose to be together (temperature, setpoint, constC, etc) need to begin with the same name, explaind in DP: Associated Items. In version 0.2 i’ve made it possible to split a room in different devices, ex I’ve in my bathroom both a radiator and under the floor heating. They work differently, so the rule can make them indepentently
The heating mode is based on the modes of the Eurotronic Spirit Zwave thermostat, but any number as mode can be used.
The parameters used in de original SVT are converted to items and groups.
Last, the rule needs a database for the item history. I use RRD4J, but it can be other databases (except MAPDB)

Old code (version 0.1)

Items:

Group:Number			Heating_Mode																						(RestoreOnStartup)																			
Group:Switch			Boiler_Valve																						(RestoreOnStartup, BoilerRoom, Influxdb)															
Group:DateTime			Heating_EndHeat																						(RestoreOnStartup)															
Group:Number			Heating_Setpoint_Normal																				(RestoreOnStartup)																			
Group:Number			Heating_Setpoint_Eco																				(RestoreOnStartup)																			
Group:Number			Heating_Setpoint_Current		                                                                    (RestoreOnStartup,Influxdb)																																									
Group:DateTime			Heating_NextCalc																					(RestoreOnStartup)															
Group:Number        	Heating_ConstC																						(RestoreOnStartup,Influxdb)
Group:DateTime       	Heating_LastCalc																					(RestoreOnStartup)											
Group:Number			Heating_CalcTime																					(RestoreOnStartup)
Group:Switch			Heating_Learn																						(RestoreOnStartup)
Number					Bathroom_Radiator_Mode				"Badkamer radiator modus [MAP(spiritmode.map):%s]"				(Bathroom, Node3, Heating_Mode )			{ channel="zwave:device:512:node3:thermostat_mode" }
Number					Bathroom_Radiator_Setpoint_Normal	"Badkamer radiator thermostaat [%.1f °C]"		<my-heating>	(Bathroom, Node3,Heating_Setpoint_Normal)	
Number					Bathroom_Radiator_Setpoint_Eco		"Badkamer radiator thermostaat eco [%.1f °C]"	<my-heating>	(Bathroom, Node3,Heating_Setpoint_Eco)		{ channel="zwave:device:512:node3:thermostat_setpoint_heating_econ" }
Number:Dimensionless	Bathroom_Radiator_Valve				"Badkamer radiator klep [JS(topct.js):%s]"						(Bathroom, Node3)							{ channel="zwave:device:512:node3:switch_dimmer" }
Number					Bathroom_Radiator_SensorReport		"Badkamer externe temperatuur [%.1f °C]"						(Bathroom,Node3)							{ channel="zwave:device:512:node3:sensor_report" }
Switch					Bathroom_Boiler_Valve																				(Boiler_Valve, BoilerRoom)															
Number					Bathroom_Heating_ConstC																				(Heating_ConstC)
DateTime				Bathroom_Heating_LastCalc																			(Heating_LastCalc)																	
DateTime				Bathroom_Heating_NextCalc																			(Heating_NextCalc)														
DateTime				Bathroom_Heating_EndHeat																			(Heating_EndHeat)														
Number					Bathroom_Heating_Setpoint_Current																	(Heating_Setpoint_Current)
Number					Bathroom_Heating_CalcTime																			(Heating_CalcTime)
Switch					Bathroom_Heating_Learn																				(Heating_Learn)

Switch					LivingRoom_Boiler_Valve																				(Boiler_Valve, BoilerRoom)															
Number					LivingRoom_Heating_Mode				"Woonkamer verwarming modus [MAP(spiritmode.map):%s"			(LivingRoom, Heating_Mode)															
Number					LivingRoom_Heating_ConstC																			(Heating_ConstC)												
DateTime				LivingRoom_Heating_LastCalc																			(Heating_LastCalc)																	
DateTime				LivingRoom_Heating_NextCalc																			(Heating_NextCalc)														
DateTime				LivingRoom_Heating_EndHeat																			(Heating_EndHeat)														
Number					LivingRoom_Heating_Setpoint_Normal	"Woonkamer thermostaat [%.1f °C]"				<my-heating>	(LivingRoom, Heating_Setpoint_Normal)												
Number					LivingRoom_Heating_Setpoint_Eco		"Woonkamer thermostaat eco [%.1f °C]"			<my-heating>	(LivingRoom, Heating_Setpoint_Eco)													
Number					LivingRoom_Heating_Setpoint_Current																    (Heating_Setpoint_Current)
Number					LivingRoom_Heating_CalcTime																			(Heating_CalcTime)
Switch					LivingRoom_Heating_Learn																			(Heating_Learn)

Switch					BoilerRoom_Boiler_Switch			"CV aan/uit" 									<fire>			(BoilerRoom,Influxdb)									

Group:Number:AVG	Temperatures						"Temperaturen"										<temperature>	(Home,Rrd4j,Influxdb)
Group:Number:AVG			Bathroom_Temperatures				"Badkamer temperatuur [%.1f °C]"								<temperature>	(Bathroom,Temperatures)			[ "CurrentTemperature","Temperature", "Measurement"]
Group:Number:AVG			LivingRoom_Temperatures				"Woonkamer temperatuur [%.1f °C]"								<temperature>	(LivingRoom,Temperatures)			[ "CurrentTemperature","Temperature", "Measurement"]
Number				Bathroom_Radiator_Temperature		"Badkamer radiator temperatuur [%.1f °C]"		<temperature>	(Bathroom, Bathroom_Radiator_Thermostat,Temperatures, Bathroom_Temperatures, Node3)	[ "CurrentTemperature","Temperature", "Measurement"]			{ channel="zwave:device:512:node3:sensor_temperature" }
Number				Bathroom_Sensor_Temperature 		"Badkamer sensor temperatuur [%.1f °C]"			<temperature>	(Bathroom,Temperatures,Bathroom_Temperatures,Node17)								[ "CurrentTemperature","Temperature", "Measurement"]		{channel="zwave:device:512:node17:sensor_temperature"}
Number 				LivingRoom_EyeSensor_Temperature	"Woonkamer temperatuur [%.1f °C]"							<temperature>	(LivingRoom,LivingRoom_Temperatures, Node15)				[ "CurrentTemperature" ]			{ channel="zwave:device:512:node15:sensor_temperature" }

Rules:

rule "Smart Heating"
when
	Time cron "0/10 * * ? * * *"or
	Member of Heating_Learn received update OFF  //force rule to run when setpoint/mode changes
then
	val delta=0.2
	Heating_Mode.allMembers.forEach[ modeItem |
		val room = modeItem.name.split("_").get(0)
		val valveItem = Boiler_Valve.allMembers.findFirst[name.contains(room)]
		logInfo("SVT","Running rule for room: {}, Mode is {}, Valve is {}",room,modeItem.state.toString,valveItem.state.toString)
		if (modeItem.state == NULL || modeItem.state==0 ){
			if (valveItem.state != OFF){
				valveItem.sendCommand(OFF)
			}
			if (modeItem.state != 0){
				modeItem.postUpdate(0)
			}
			return;
		}
		val endHeatItem = Heating_EndHeat.allMembers.findFirst[name.contains(room)]
		logInfo("SVT","End heat for room {} at {}",room,endHeatItem.state.toString)
		if (endHeatItem.state == NULL || (now.isAfter(new DateTime(endHeatItem.state.toString)) && valveItem.state != OFF )){
			endHeatItem.postUpdate(new DateTimeType(now.toString))
			valveItem.sendCommand(OFF)
			return;
		}
		val nextCalcItem = Heating_NextCalc.allMembers.findFirst[name.contains(room)]
		if (nextCalcItem.state == NULL) {nextCalcItem.postUpdate(new DateTimeType(now.toString))} 
		val calc_time=(Heating_CalcTime.allMembers.findFirst[name.contains(room)].state as Number).intValue
		logInfo("SVT","Next calculation for room {} at {}",room,nextCalcItem.state.toString)
		if (now.isAfter(new DateTime(nextCalcItem.state.toString))){
			logInfo("SVT","Starting calculation for room {}",room)
			nextCalcItem.postUpdate(now.plusMinutes(calc_time).toString)
			val temp = Temperatures.allMembers.filter[name.contains(room)].map[(state as Number).doubleValue].reduce[s, v |s+v]/Temperatures.allMembers.filter[name.contains(room)].size
			val setpointItem = Heating_Setpoint_Current.allMembers.findFirst[name.contains(room)]
			var double setpoint = 0
			var double power = 0 
			switch modeItem.state {
				case 1 :	{
					setpointItem.postUpdate(Heating_Setpoint_Normal.allMembers.findFirst[name.contains(room)].state.toString)
					setpoint = (Heating_Setpoint_Normal.allMembers.findFirst[name.contains(room)].state as Number).doubleValue
				}
				case 11:	{
					setpointItem.postUpdate(Heating_Setpoint_Eco.allMembers.findFirst[name.contains(room)].state.toString)
					setpoint = (Heating_Setpoint_Eco.allMembers.findFirst[name.contains(room)].state as Number).doubleValue
				}
				case 15: {
					power=50.0 //force the heating ON for half the calculation time 
				}
			}
			logInfo("SVT","Current temperature {}, current setpoint {}, room {}",temp.toString,setpoint.toString,room)
			val lastCalcItem=Heating_LastCalc.allMembers.findFirst[name.contains(room)]
			if (temp <= (setpoint+delta)){
				logInfo("SVT","adjusting power for room {}",room)
				val learn = Heating_Learn.allMembers.findFirst[name.contains(room)]
				val constCItem = Heating_ConstC.allMembers.findFirst[name.contains(room)]
				var double constC
				logInfo("SVT","Learn switch {}, ConstC {}, Room {}",learn.state.toString,constCItem.state.toString,room)
				if (learn.state == ON){
					constC = (constCItem.averageSince(now.minusDays(7),"influxdb") as Number).doubleValue
					val lastSetpoint = (setpointItem.previousState(false,"influxdb").state as Number).doubleValue
					val lastTemp = Temperatures.allMembers.filter[name.contains(room)].map[(previousState(false,"influxdb").state as Number).doubleValue].reduce[s, v |s+v]/Temperatures.allMembers.filter[name.contains(room)].size
					val deltaSeconds = ((now.millis-(new DateTime(lastCalcItem.state.toString)).millis)/1000).doubleValue
					logInfo("SVT","Last temperature {}, Last setpoint {}, room {}",lastTemp.toString,lastSetpoint.toString,room)
					constC = constC * ((lastSetpoint - lastTemp)/(temp-lastTemp)*deltaSeconds/(calc_time*60))
				}else{
					learn.postUpdate(ON)
					constC = (constCItem.state as Number).doubleValue
				}
				logInfo("SVT","new ConstC {} for {}",constC.toString,room)
				power = (setpoint-temp)*constC
				constCItem.postUpdate(constC) 
				logInfo("SVT","power {}",power)
			}
			if (power > 100){ 
				power=100  //upperlimit
			}
			val heatduration=(power*calc_time/100).intValue
			logInfo("SVT","heatduration {}",heatduration.toString)
			if (heatduration==0){
				valveItem.sendCommand(OFF)  //no heat request
			}else{
				endHeatItem.postUpdate(new DateTimeType(now.plusMinutes(heatduration).toString))
				valveItem.sendCommand(ON)

			}
			lastCalcItem.postUpdate(new DateTimeType(now.toString))

		}

	]
end 
rule "Setpoint change"
when
	Member of Heating_Setpoint_Normal changed
	or
	Member of Heating_Setpoint_Eco changed 
	or
	Member of Heating_Mode changed
then
	val room = triggeringItem.name.split("_").get(0)
	val nextCalcItem = Heating_NextCalc.allMembers.findFirst[name.contains(room)]
	val learn = Heating_Learn.allMembers.findFirst[name.contains(room)]
	nextCalcItem.postUpdate(new DateTimeType(now.toString))
	learn.postUpdate(OFF)

end

rule "Boiler on/off"
when
	Time cron "0 0/10 * ? * * *"
then
	if (Boiler_Valve.allMembers.filter[state == ON].size > 0){
		val boilerPS = BoilerRoom_Boiler_Switch.previousState(false,"influxdb")
		if (now.minusMinutes(15).isAfter(new DateTime(boilerPS.timestamp.time)) && boilerPS.state == OFF ){ //prevent on/off flapping
			BoilerRoom_Boiler_Switch.sendCommand(ON)
		}
	}else{
		BoilerRoom_Boiler_Switch.sendCommand(OFF)
	}
end

Heatingmode map:

0=Off
1=Heat
11=Economy Heat
15=Force Heat

New version (0.2.2)
Items:

Group:Number			Heating_Mode																						(RestoreOnStartup)																			
Group:Switch:OR(ON,OFF)			Heating_BoilerValves																		(RestoreOnStartup, BoilerRoom, Influxdb)															
Group:Number			Heating_Setpoint_Normal																				(RestoreOnStartup)																			
Group:Number			Heating_Setpoint_Eco																				(RestoreOnStartup)																			
Group:Number			Heating_Setpoint_Current		                                                                    (RestoreOnStartup,Influxdb,Rrd4j)																																									
Group:Number        	Heating_ConstC																						(RestoreOnStartup,Influxdb,Rrd4j)
Group:DateTime       	Heating_LastCalc																					(RestoreOnStartup)											
Group:Number			Heating_CalcTime																					(RestoreOnStartup)
Group:Switch			Heating_Learn																						(RestoreOnStartup)


Number					Bathroom_Radiator_Mode				"Badkamer radiator modus [MAP(spiritmode.map):%s]"				(Bathroom, Node3, Heating_Mode )			{ channel="zwave:device:512:node3:thermostat_mode" }
Number					Bathroom_Radiator_Setpoint_Normal	"Badkamer radiator thermostaat [%.1f °C]"		<my-heating>	(Bathroom, Node3,Heating_Setpoint_Normal)	{ channel="zwave:device:512:node3:thermostat_setpoint_heating" }
Number					Bathroom_Radiator_Setpoint_Eco		"Badkamer radiator thermostaat eco [%.1f °C]"	<my-heating>	(Bathroom, Node3,Heating_Setpoint_Eco)		{ channel="zwave:device:512:node3:thermostat_setpoint_heating_econ" }
Number:Dimensionless	Bathroom_Radiator_Valve				"Badkamer radiator klep [JS(topct.js):%s]"						(Bathroom, Node3)							{ channel="zwave:device:512:node3:switch_dimmer" }
Number					Bathroom_Radiator_SensorReport		"Badkamer externe temperatuur [%.1f °C]"						(Bathroom, Node3)							{ channel="zwave:device:512:node3:sensor_report" }
Switch					Bathroom_Radiator_BoilerValve				"Badkamer vloerverwarmingsklep"							(Heating_BoilerValves, BoilerRoom)
Number					Bathroom_Radiator_Setpoint_Current																	(Heating_Setpoint_Current)
Number					Bathroom_Radiator_CalcTime																			(Heating_CalcTime)
Number					Bathroom_Radiator_ConstC																			(Heating_ConstC)
DateTime				Bathroom_Radiator_LastCalc																			(Heating_LastCalc)																	
Switch					Bathroom_Radiator_Learn																				(Heating_Learn)

Number					Bathroom_Floor_Mode				"Badkamer vloerverwarming modus [MAP(spiritmode.map):%s]"		(Bathroom, Heating_Mode )
Number					Bathroom_Floor_Setpoint_Normal	"Badkamer vloerverwarming thermostaat [%.1f °C]"		<my-heating>	(Bathroom, Node3,Heating_Setpoint_Normal)	
Number					Bathroom_Floor_Setpoint_Eco		"Badkamer vloerverwarming thermostaat eco [%.1f °C]"	<my-heating>	(Bathroom, Node3,Heating_Setpoint_Eco)		
Switch					Bathroom_Floor_BoilerValve				"Badkamer vloerverwarmingsklep"								(Heating_BoilerValves, BoilerRoom, Node18)			{ channel="zwave:device:512:node18:switch_binary1" }
Number					Bathroom_Floor_Setpoint_Current																	(Heating_Setpoint_Current)
Number					Bathroom_Floor_CalcTime																			(Heating_CalcTime)
Number					Bathroom_Floor_ConstC																			(Heating_ConstC)
DateTime				Bathroom_Floor_LastCalc																			(Heating_LastCalc)																	
Switch					Bathroom_Floor_Learn																				(Heating_Learn)

Switch					LivingRoom_Heating_BoilerValve																				(Heating_BoilerValves, BoilerRoom)															
Number					LivingRoom_Heating_Mode				"Woonkamer verwarming modus [MAP(spiritmode.map):%s"			(LivingRoom, Heating_Mode)															
Number					LivingRoom_Heating_ConstC																			(Heating_ConstC)												
DateTime				LivingRoom_Heating_LastCalc																			(Heating_LastCalc)																	
Number					LivingRoom_Heating_Setpoint_Normal	"Woonkamer thermostaat [%.1f °C]"				<my-heating>	(LivingRoom, Heating_Setpoint_Normal)												
Number					LivingRoom_Heating_Setpoint_Eco		"Woonkamer thermostaat eco [%.1f °C]"			<my-heating>	(LivingRoom, Heating_Setpoint_Eco)													
Number					LivingRoom_Heating_Setpoint_Current																    (Heating_Setpoint_Current)
Number					LivingRoom_Heating_CalcTime																			(Heating_CalcTime)
Switch					LivingRoom_Heating_Learn																			(Heating_Learn)

Switch					BoilerRoom_Boiler_Switch			"CV aan/uit" 									<fire>			(BoilerRoom,Influxdb)									{ channel="zwave:device:512:node18:switch_binary1" }


Group:Number:AVG	Temperatures						"Temperaturen"										<temperature>	(Home,Rrd4j,Influxdb)
Group:Number:AVG			Bathroom_Temperatures				"Badkamer temperatuur [%.1f °C]"								<temperature>	(Bathroom,Temperatures)			[ "CurrentTemperature","Temperature", "Measurement"]
Group:Number:AVG			LivingRoom_Temperatures				"Woonkamer temperatuur [%.1f °C]"								<temperature>	(LivingRoom,Temperatures)			[ "CurrentTemperature","Temperature", "Measurement"]
Number				Bathroom_Radiator_Temperature		"Badkamer radiator temperatuur [%.1f °C]"		<temperature>	(Bathroom, Bathroom_Radiator_Thermostat,Temperatures, Bathroom_Temperatures, Node3)	[ "CurrentTemperature","Temperature", "Measurement"]			{ channel="zwave:device:512:node3:sensor_temperature" }
Number				Bathroom_Sensor_Temperature 		"Badkamer sensor temperatuur [%.1f °C]"			<temperature>	(Bathroom,Temperatures,Bathroom_Temperatures,Node17)								[ "CurrentTemperature","Temperature", "Measurement"]		{channel="zwave:device:512:node17:sensor_temperature"}
Number 				LivingRoom_EyeSensor_Temperature	"Woonkamer temperatuur [%.1f °C]"							<temperature>	(LivingRoom,LivingRoom_Temperatures, Node15)				[ "CurrentTemperature" ]			{ channel="zwave:device:512:node15:sensor_temperature" }

Rules:


val heatingTimers = <String, Timer>newHashMap

val checkFirstS=[SwitchItem item,OnOffType command,int minutes|
		if (now.minusMinutes(minutes).isAfter(new DateTime(item.previousState(false,"influxdb").timestamp.time))) {  //changed since to avoid flapping in minutes amount of time	
			if (item.state != command) item.sendCommand(command)
		}else{
			val timeritem = item
			val timercommand = command
			createTimer(now.plusMinutes(minutes), [|
				if (timeritem.state != command) timeritem.sendCommand(timercommand)
			])
		}
	]


rule "Smart heating v0.2.2"
/* version 0.2.2:	bugfixes
					infinity calculation catch -> probaly because last temperature == current temperature, fixed with previousstate(true)
*/			

when
	Member of Heating_Setpoint_Normal received update
	or
	Member of Heating_Setpoint_Eco received update
	or
	Member of Heating_Mode received update
	or
	Member of Heating_CalcTime received update
	or
	System started

then
	val DELTA=0.2
	val DATABASE="influxdb"
	val room = 
		if (triggeringItem === null){ //system started trigger
			logInfo("svt2","System started, resetting timers and checking for NULL")
			Heating_BoilerValves.allMembers.forEach[if (state == NULL) postUpdate(OFF)]
			Heating_CalcTime.allMembers.forEach[if (state == NULL) postUpdate(30)]
			Heating_ConstC.allMembers.forEach[if (state == NULL) postUpdate(60)]
			Heating_LastCalc.allMembers.forEach[if (state == NULL) postUpdate(new DateTimeType(now.toString))]
			Heating_Learn.allMembers.forEach[if (state == NULL) postUpdate(ON)]
			Heating_Mode.allMembers.forEach[if (state == NULL) postUpdate(0)]
			Heating_Setpoint_Current.allMembers.forEach[if (state == NULL) postUpdate(20)]
			Heating_Setpoint_Eco.allMembers.forEach[if (state == NULL) postUpdate(17)]
			Heating_Setpoint_Normal.allMembers.forEach[if (state == NULL) postUpdate(20)]
			createTimer(now.plusSeconds(10),[|	//let OH give a chance to update NULLs
				logInfo("svt2","retrigger")
				Heating_Mode.allMembers.forEach[i|
					i.sendCommand(i.state)
					logInfo("svt2","retrigger {}",i.name)
					] 
			])
			return;
		}else{
			triggeringItem.name.split("_").get(0)
		}
	val roomDevice = room+"_"+triggeringItem.name.split("_").get(1)
	logInfo("svt2","Room {}, RoomDevice {}",room, roomDevice)
	if (Heating_Mode.allMembers.findFirst[name.contains(roomDevice)].state.toString == "0"){
		logInfo("svt2","Heating mode for {} is off",roomDevice)
		heatingTimers.get(roomDevice+"_NextCalc")?.cancel
		heatingTimers.put(roomDevice+"_NextCalc", null)
		heatingTimers.get(roomDevice+"_EndHeat")?.cancel
		heatingTimers.put(roomDevice+"_EndHeat", null)
		checkFirstS.apply(Heating_BoilerValves.allMembers.findFirst[name.contains(roomDevice)],OFF,1)
		return;
	}
	val roomNextCalcTimer = heatingTimers.get(roomDevice+"_NextCalc")
	logInfo("svt2","nextCalcTimer for {} is {}",roomDevice,roomNextCalcTimer?.toString)
	val calc_time=(Heating_CalcTime.allMembers.findFirst[name.contains(roomDevice)].state as Number).intValue
	//val calc_time=1 //debug
	if (roomNextCalcTimer === null || roomNextCalcTimer.hasTerminated){
		logInfo("svt2","start new calc for {} in {} minutes",roomDevice,calc_time)
		heatingTimers.put(roomDevice+"_NextCalc", createTimer(now, [|  //start now instead of calc_time
			val roomTemp = Temperatures.allMembers.filter[name.contains(room)].map[(state as Number).doubleValue].reduce[s, v |s+v]/Temperatures.allMembers.filter[name.contains(room)].size
			val roomMode = Heating_Mode.allMembers.findFirst[name.contains(roomDevice)].state.toString
			val lastCalcItem = Heating_LastCalc.allMembers.findFirst[name.contains(roomDevice)]
			val roomSetpoint = 
				switch roomMode {
					case "1": {
						(Heating_Setpoint_Normal.allMembers.findFirst[name.contains(roomDevice)].state as Number).doubleValue
					}
					case "11": {
						(Heating_Setpoint_Eco.allMembers.findFirst[name.contains(roomDevice)].state as Number).doubleValue
					}
					case "15" :{
						0  //force mode, but this won't recalculate the ConstC
					}
				}
			logInfo("svt2","Roomdevice {}, Setpoint {}, Temp {}, Mode {}, Learning {}",roomDevice,roomSetpoint,roomTemp,roomMode,Heating_Learn.allMembers.findFirst[name.contains(roomDevice)].state.toString)
			val roomLearnItem = Heating_Learn.allMembers.findFirst[name.contains(roomDevice)]
			val power = 
				if (roomTemp <= (roomSetpoint+DELTA)){
					val constCItem = Heating_ConstC.allMembers.findFirst[name.contains(roomDevice)]
					val cc = (constCItem.averageSince(now.minusDays(7),DATABASE) as Number).doubleValue
					val constC =
						if (roomLearnItem.state == ON){
							val setpointItem = Heating_Setpoint_Current.allMembers.findFirst[name.contains(roomDevice)]
							val lastSetpoint = (setpointItem.previousState(false,DATABASE).state as Number).doubleValue
							val lastTemp = Temperatures.allMembers.filter[name.contains(room)].map[(previousState(true,DATABASE).state as Number).doubleValue].reduce[s, v |s+v]/Temperatures.allMembers.filter[name.contains(room)].size  //get average of room temperatures (group), but it must not be the same value as current
							val deltaSeconds = ((now.millis-(new DateTime(lastCalcItem.state.toString)).millis)/1000).doubleValue
							logInfo("svt2","Last temperature {}, Last setpoint {}, delta {}, room temp {}, calc time {}, ConstCAvg {}",lastTemp.toString,lastSetpoint.toString,deltaSeconds,roomTemp,calc_time,(constCItem.averageSince(now.minusDays(7),DATABASE) as Number))
							val ccCalc= ((lastSetpoint - lastTemp)/(roomTemp-lastTemp)*deltaSeconds/(calc_time*60))
							if (ccCalc.toString.contains("Infinity")){ //catch if somehow calculation is infinity
								logInfo("svt2","Infinity calc, roomDevice: {}",roomDevice)
								Heating_ConstC.allMembers.findFirst[name.contains(roomDevice)].postUpdate(cc)
								cc
							}else{
								Heating_ConstC.allMembers.findFirst[name.contains(roomDevice)].postUpdate(cc*ccCalc)
								cc * ccCalc
							}
						}else{
							cc
						}
					logInfo("svt2","ConstC is {}",constC)
					var pw = (roomSetpoint-roomTemp)*constC
					if (pw>100) pw=100 //max
					if (pw<0) pw=0 //min
					pw
				}else{
					if (roomSetpoint == 0){ //force mode
						100
					}else{
						0
					}
				}
			roomLearnItem.postUpdate(ON)
			logInfo("svt2","Power is {}",power)
			val heatduration=(power*calc_time/100).intValue
			val valveItem = Heating_BoilerValves.allMembers.findFirst[name.contains(roomDevice)]
			logInfo("svt2","Heatduration is {} minutes for {}",heatduration.toString,roomDevice)
			val roomEndHeatTimer = heatingTimers.get(roomDevice+"_EndHeat")
			logInfo("svt2","endHeatTimer for {} is {}",roomDevice,roomNextCalcTimer?.toString)
			if (power==0){
				logInfo("svt2","No heat request")
				checkFirstS.apply(valveItem,OFF,1)
				roomEndHeatTimer?.cancel
				heatingTimers.put(roomDevice+"_EndHeat",null)
			}else{
				checkFirstS.apply(valveItem,ON,1)
				logInfo("svt2","Valve ON")
				Heating_Setpoint_Current.allMembers.findFirst[name.contains(roomDevice)].postUpdate(roomSetpoint)
				if (roomEndHeatTimer === null || roomEndHeatTimer.hasTerminated){
					heatingTimers.put(roomDevice+"_EndHeat", createTimer(now.plusMinutes(heatduration),[|
						logInfo("svt2","End heat")
						if (power<100) {
							checkFirstS.apply(valveItem,OFF,1)
							logInfo("svt2","Valve OFF {}",roomDevice)
						}
					]))
				}else{
					logInfo("svt2","Timer end heat active, reschedule for {} minute(s)",heatduration)
					roomEndHeatTimer.reschedule(now.plusMinutes(heatduration))
				}
			}
		lastCalcItem.postUpdate(new DateTimeType(now.toString))
		heatingTimers.get(roomDevice+"_NextCalc").reschedule(now.plusMinutes(calc_time))
		logInfo("svt2","reschedule nextCalcTimer for {} is {}. Calctime {}. Inside own timer",roomDevice,heatingTimers.get(roomDevice+"_NextCalc")?.toString,calc_time)
		]))
	}else{
		if (triggeringItem.name.contains("Setpoint") || triggeringItem.name.contains("Mode")){
			Heating_Learn.allMembers.findFirst[name.contains(roomDevice)].sendCommand(OFF)
			roomNextCalcTimer.reschedule(now)
			logInfo("svt2","reschedule nextCalcTimer for {} is {}. CalcTime {}. When mode/setpoint changed",roomDevice,roomNextCalcTimer?.toString,calc_time)
		}else{
			roomNextCalcTimer.reschedule(calc_time)
			logInfo("svt2","reschedule nextCalcTimer for {} is {}. Calctime {}",roomDevice,roomNextCalcTimer?.toString,calc_time)
		}
	}
end

rule "Boiler on/off"
when
	Member of Heating_BoilerValves changed
then
	checkFirstS.apply(BoilerRoom_Boiler_Switch,Heating_BoilerValves.state,10)
	logInfo("Boiler","CV {}",Heating_BoilerValves.state.toString)
end

Heatingmode map:

0=Off
1=Normal Heat
11=Economy Heat
15=Force Heat

Looking forward to see your comments :smiley:

4 Likes

@rlkoshak is our DP wizard here :rofl:

This is a great tutorial but I don’t think it’s a DP. A DP is really for a common way to solve lots of different problems where as this is really only applicable to thermostats. So I wouldn’t worry about trying to turn it into a DP.

In a tutorial like this, you should check for NULL which appears to be the case. But don’t forget UNDEF too. Be sure to log out a meaningful error message or warning when a NULL or UNDEF Item is encountered preventing the Rule from successfully running.

Please explain why not RRD4J. I assume it’s because it can’t handle UoM?

As a general rule, seeing a Rule that runs every X seconds is a code smell. OH is an event driven system so it is usually more appropriate to trigger the rule based on events instead of on a fixed polling period. For example, when temperatures change and when the target temperatures change. If there are no changes there is nothing for the Rule to do, so why run it just because ten seconds have passed?

Everyone has their own coding style, but the most common style has constants named with all caps. And this particular constant probably should be a global val, though that too is more of a style thing.

Do you really need to run the Rule for all rooms every time? If you moved your triggers to trigger on temp and target temp/mode changes then you can use triggeringItem.name.split... to get the name of the room that changed and only run the Rule for that room. That will simplify the Rule a bit.

Log a warning at least here to indicate why the Rule didn’t do anything. It’s not as big of a deal when you are not running the rule every ten seconds.

logWarn("smart heating", "Setting {} to OFF and {} to 0 because modeItem is {}", valveItem.name, modeItem.name, modeItem.state)

Review Design Pattern: How to Structure a Rule. In a Rule like this typically you have a bunch of if statements or case statements that all do the same thing but with different values. So instead just calculate the values but actually make the changes (e.g. postUpdate, sendCommand) once at the end of the Rule. This can simplify the Rule, reduces duplicated code, and make it easier to maintain and change later when you decide the Rule needs to do something else.

To make it easier for users to adopt the Rule, make “influxdb” a constant the user can set to their own database in one place.

Please provide a “theory of operation” where you describe in prose what these Rules do, step by step. Especially for long and complicated Rules like this it helps one figure out what the Rule does more easily. I admit, I really don’t know how this Rule works or what it’s really doing. Perhaps it’s described in the link. It might help to add some comments to the major sections of the complicated Rule that explains at a high level what that section is doing.

That’s all I have right now. I don’t have time to really look into the rule for logical errors or edge cases. I was only able to look at it from a shallow perspective.

Thanks for posting! It looks like a useful tutorial that I’m sure others will find useful.

3 Likes

Thank you @rlkoshak for your feedback! I’ll try to explain/answer your questions/feedback and eventually I’ll update my code in the first post.

Because I noticed that values of an item in a specific time in the past, not possible is with RRD4J, because of the nature of the database. I’ll recheck it again, because for number items it is using less resources.

This is a very good point! Here I made a mistake not thinking of the nature of OH (event driven) but just copying/translating from the source. Domoticz doesn’t work (as far as I know) event driven. The ten seconds is the “heartbeat” that Domoticz send to a script (see Domoticz Callbacks). And with this thinking I made it for every room.

I’m going to change this to the OH manner, with the triggers as you suggested.

Noted, I’ll review the DP.

Good suggestion. Never thought that influxdb is just a string and it can be a variable.

The way that rrd4j works is that as the data ages, high resolution data (e.g. one value every minute) gets replaced with an average of entries over a given time period (e.g. the average of every ten minutes). But there is always a value at any time in the past, but depending on the age of the data the value may not be exactly what it was at that time in real life. But unless the values are wildly jumping from a low value to a high value from one reading to the next, the value will be very close to the original. You are not going back that far into the past so rrd4j values should be just fine for this use case.

1 Like

This looks really promising! I’ve been having a little bit of overshooting with my heating solution, this could be a cool change to make.

I’ll have a look later, and maybe swap over a room to test it out :slight_smile:

I’ve just updated the code in the starter post
I’ve completely rewritten the code to a more OpenHAB design, based on the advise of @rlkoshak :+1:

Please let me know what you think of the new version.

2 Likes

I’m very interrested in this…
Right now I have some Danfoss TRV, but wanna changed to some Eurotronic Spirit and implement this or something like this.
Haven’t got the chance to look deeper in the code, but may I suggest adding some comments? They can be helpful to understand the logic :smile:.
Also, another suggestion, for a future version. Think of implementing an anti-calcification mechanism for the summer period - is “anti-calcification” a word? Sorry, English is not my strength, but maybe you understand what I mean… Something like opening and closing the valve 2-3 times once a week when heating is off?

I’d like to try to understand this a little more, could do with some comments added in the code.

In addition, there appears that there may be some extra groups etc defined against the items, plus the descriptions are not in English, so I’m not sure what is needed and what isn’t, like which groups are necessary for this, and what are there just for your installation.

Thanks!

I know, I’m not that good in adding comments. I’ll try to do it more and more.

As far as I know, this is not necessary with the Eurospirit, maybe you can tackle this with a separate rule if necessary.

These groups Influxdb, Influxdb_EU, Rrd4j, RestoreOnStartup are based on this Design Pattern: Group Based Persistence.
NodeX is my Zwave group (all items that belongs to Zwave device Node X are in a group) and Home, BoilerRoom, Bathroom, etc are groups for my house layout (Floors, rooms, etc)
The labels are in Dutch, but all comments and item names are in English (as far as I know :grimacing:). I think it’s time consuming to translate all the labels and you also have Google Translate :wink:

I tried to get this up and running last night, but there seems to be some missing steps to get this working correctly. I was getting a number of errors due to NULL, and then when it appeared to do something more, I had a target of 24, with current temp of ~22 (and gradually dropping), and over the course of an hour, it never said it needed heat, Power was always 0.

Maybe because the room was hotter than the target when I first started? Does it need to start from “cold”?

I appreciate the effort you’ve put in, and I’m sure it’s working for you… but trying to configure this on a fresh installation causes problems!

I would appreciate if you could try what you’ve got above on a fresh install, hopefully you’ll get similar issues to me, and can more quickly fix them then I can! (I think I stumbled upon something vaguely working more by luck than doing anything by judgement!)

Thanks for trying! Good advice to try it on a fresh install. I got also NULL errors:

2019-11-07 16:25:53.960 [WARN ] [nce.extensions.PersistenceExtensions] - There is no queryable persistence service registered with the id 'influxdb'
2019-11-07 16:25:53.961 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Smart heating v0.2.2': cannot invoke method public abstract java.util.Date org.eclipse.smarthome.core.persistence.HistoricItem.getTimestamp() on null
2019-11-07 16:25:53.963 [WARN ] [nce.extensions.PersistenceExtensions] - There is no queryable persistence service registered with the id 'influxdb'
2019-11-07 16:25:53.964 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Smart heating v0.2.2': cannot invoke method public abstract java.util.Date org.eclipse.smarthome.core.persistence.HistoricItem.getTimestamp() on null

But this was because I forget to install a persistence add-on.
I’ll continue to test on a fresh install.

1 Like

Can you share a logfile @Confused with the null errors?
I’ve copy and paste my rules and items as posted here in a new and fresh openhab (docker version 2.5M4). The only null-errors i’ve got came from the persistence. As I mentioned, I forget to install a persistence (i’ve used rrd4j for testing).
Make sure that you have temperature sensors using the same persistence. Otherwise it won’t work either (because of the previousState).