Smart Virtual Thermostat (beta version)

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 (default 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 version for reference

Version (0.2.5.1)
Items:

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


Group:Number			Bathroom_Heating_Mode				"Badkamer modus [MAP(spiritmode.map):%s]"						(Bathroom)
Group:Number			Bathroom_Heating_Setpoint_Normal	"Badkamer thermostaat [%.1f °C]"				<myheating>	(Bathroom)	
Group:Number			Bathroom_Heating_Setpoint_Eco		"Badkamer thermostaat eco [%.1f °C]"			<myheating>	(Bathroom)		
Switch					Bathroom_Heating_Timer				"Badkamer verwarming 1 uur aan"						<fire>			(Bathroom)											{ expire="1h, command=OFF"}

Number					Bathroom_Radiator_Mode				"Badkamer radiator modus [MAP(spiritmode.map):%s]"				(Bathroom, Node3, Heating_Mode, Bathroom_Heating_Mode)			{ channel="zwave:device:512:node3:thermostat_mode" }
Number					Bathroom_Radiator_Setpoint_Normal	"Badkamer radiator thermostaat [%.1f °C]"		<myheating>	(Bathroom, Node3, Heating_Setpoint_Normal, Bathroom_Heating_Setpoint_Normal)	{ channel="zwave:device:512:node3:thermostat_setpoint_heating" }
Number					Bathroom_Radiator_Setpoint_Eco		"Badkamer radiator thermostaat eco [%.1f °C]"	<myheating>	(Bathroom, Node3, Heating_Setpoint_Eco, Bathroom_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 radiator virtuele klep"								(Heating_BoilerValves, BoilerRoom)
Number					Bathroom_Radiator_Setpoint_Current																	(Heating_Setpoint_Current)
Number					Bathroom_Radiator_CalcTime																			(Heating_CalcTime)
Number					Bathroom_Radiator_ConstC			"Badkamer radiator constante [%.3f]"							(Heating_ConstC)
DateTime				Bathroom_Radiator_LastCalc																			(Heating_LastCalc)																	
Switch					Bathroom_Radiator_Learn																				(Heating_Learn)
Number                  Bathroom_Radiator_Power                                                                             (Heating_Power)

Number					Bathroom_Floor_Mode					"Badkamer vloerverwarming modus [MAP(spiritmode.map):%s]"		(Bathroom, Heating_Mode, Bathroom_Heating_Mode)
Number					Bathroom_Floor_Setpoint_Normal		"Badkamer vloerverwarming thermostaat [%.1f °C]"		<myheating>	(Bathroom, Heating_Setpoint_Normal, Bathroom_Heating_Setpoint_Normal)	
Number					Bathroom_Floor_Setpoint_Eco			"Badkamer vloerverwarming thermostaat eco [%.1f °C]"	<myheating>	(Bathroom, Heating_Setpoint_Eco, Bathroom_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				"Badkamer vloerverwarming constante [%.3f]"							(Heating_ConstC)
DateTime				Bathroom_Floor_LastCalc																				(Heating_LastCalc)																	
Switch					Bathroom_Floor_Learn																				(Heating_Learn)
Number                  Bathroom_Floor_Power                                                                             (Heating_Power)

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)
Number                  LivingRoom_Heating_Power                                                                             (Heating_Power)

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.5.1"
/* version 0.2.2:	bugfixes
					infinity calculation catch -> probaly because last temperature == current temperature, fixed with previousstate(true)
	version 0.2.3:	Calculation of ConstC not correct, catch of negative numbers and when there is no heating -> no learning next time
					Solution inifity calculation won't work, previousState(true) can give a wrong measurement -> changed to historicState(last calculation)
	version 0.2.4:	ConstC gives high values, original pytoh script won't calculate if power=0 or 100 -> implemented				
					Missing update Setpoint_Current (needed for persistence)
	version 0.2.5:  bugfixes
					Prevent that ConstC will be negative
					add extra comments
	version 0.2.5.1: small bugfix with ConstC (ConstC was 0 instead of average ConstC if learning=OFF)
*/			

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="rrd4j" //"influxdb"
	val room =  //which room requires heating. If no item triggered  the rule (system started), check for nulls
		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) //sub device in room (if multiple heating devices are used)
	logInfo("svt2","Room {}, RoomDevice {}",room, roomDevice)
	if (Heating_Mode.allMembers.findFirst[name.contains(roomDevice)].state.toString == "0"){  // if heating mode =0 (off) cancel everything, stop heating and return
		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")  //Every x minutes there will be a calculation
	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){ //calculation ended or never started, so create it and run it now
		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 =  //get setpoint based on mode (normal, eco or forced)
				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
					}
				}
			val roomLearnItem = Heating_Learn.allMembers.findFirst[name.contains(roomDevice)]
			var roomLearn = roomLearnItem.state
			logInfo("svt2","Roomdevice {}, Setpoint {}, Temp {}, Mode {}, Learning {}",roomDevice,roomSetpoint,roomTemp,roomMode,roomLearn.toString)
			val setpointItem = Heating_Setpoint_Current.allMembers.findFirst[name.contains(roomDevice)]
			val powerItem = Heating_Power.allMembers.findFirst[name.contains(roomDevice)]
			var lastSetpoint = roomSetpoint // set lastsetpoint to roomsetpoint, just to besure if lastsetpoint===null
			val lastSetpointState = setpointItem.historicState(new DateTime(lastCalcItem.state.toString),DATABASE)?.state
			if (lastSetpointState !== null){
				lastSetpoint = (lastSetpointState as Number).doubleValue
			}
			val lastPower = (powerItem.historicState(new DateTime(lastCalcItem.state.toString),DATABASE).state as Number)
			if ((lastPower == 100 && roomTemp < lastSetpoint) || lastPower == 0){  //last time power = 100 and setpoint not reached -> no learning.
				roomLearn = OFF
			}
			logInfo("svt2","Roomdevice {}, roomtemp {}, roomsetpoint {}, lastpower {}, lastsetpoint {}, roomlearn {}",roomDevice, roomTemp, roomSetpoint, lastPower, lastSetpoint,roomLearn )
			val power = //determine if heating is necessary and for how long (will be used later for time calculation)
				if (roomTemp <= (roomSetpoint+DELTA)){
					val constCItem = Heating_ConstC.allMembers.findFirst[name.contains(roomDevice)]
					var cc = (constCItem.averageSince(now.minusDays(7),DATABASE) as Number).doubleValue
					if (cc<0) cc=1
					val constC =
						if (roomLearn == ON){
							val lastTemp = Temperatures.allMembers.filter[name.contains(room)].map[(historicState(new DateTime(lastCalcItem.state.toString),DATABASE).state as Number).doubleValue].reduce[s, v |s+v]/Temperatures.allMembers.filter[name.contains(room)].size  //get average of room temperatures (group) at the last calc time
							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 = 
								if (roomTemp > lastTemp && lastSetpoint > lastTemp){  //prevent that ConstC will be negative
									(lastSetpoint - lastTemp)/(roomTemp-lastTemp)*deltaSeconds/(calc_time*60)
								}else{
									1
								}
							cc * ccCalc
						}else{
							cc
						}
					constCItem.postUpdate(constC)
					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)  // in case turned off by mode/setpoint change or power=0
			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")
				roomLearnItem.postUpdate(OFF) // no power, no calculation of ConstC next time
				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))
		powerItem.postUpdate(power)
		setpointItem.postUpdate(roomSetpoint)
		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(now.plusMinutes(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

The most recent version of the files are here:

Looking forward to see your comments :smiley:

8 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).

Changed the first post with a new version (0.2.4). The main change is the addition of a check if the power was in the last calculation 0 or 100, so there will not be any learning. Without the check, the ConstC can become very high.

Thanks this looks really great, I’m planning on buying Eurotronic Spirit TRVs.

Just a few questions/suggestions:

  • Do you use external temp sensor in every room?
  • Can I use the internal temp sensor and it will work with it (for example if I measure the actual temp in the room once and set a good offset value)
  • How the outside temperature affects this rule? Why it is needed in the first place?
  • How does this rule exactly controls the valve? I can see that only the target temps are associated with a channel, and the mode, so it just modifies the target temp? Or does it control the valve directly (like setting the valve to 50%)
  • Also you have group Items for Bathroom and not for the Living Room. Why it is different in each room? What items are a ‘must have’?
    Can you extend the code with more comments? Like what happens at each block. This looks like a rather complex rule and that way maybe customizing it would be easier.

Thanks for your work!

Since a short time I use a external temp sensor Heiman with a direct association with the Spirit.
Before that I’ve used the internal sensor. It works great, but the internal sensor you have to adjust, because it’s near the heat source (obviously).

The outside temperature is not yet affecting the rule (feature for later). The idea is that if you have a house which is not so isolated, the rule can adjust the power based on inside and outside temperatures -> with other words, if it’s cold outside, the heating needs to be longer as opposite when it’s outside warmer.
My house is a new house (2 years old) en very good isolated, so the need of the outside temperature is for my case not very important.

The valve does it own job. It calculates bases on the internal or external sensor what the opening of the valve must be, just like a regular radiator thermostat (only that’s based on thermal expansion).

In my bathroom I’ve two “heating devices” -> a radiator and under the floor heating. I’ve created a group to control both of them at the same time. This is not necessary. The must have items are the items which the last part of the name is the same as the group.
The item names contains: ROOM_ANYNAME_FUNCTION where ROOM is obvious, ANYNAME you can give it what you like (ex Floor, Radiator, Towel, Upper, Lower, etc etc) and Function is one of these: Mode, BoilerValve, Setpoint_Normal, Setpoint_Eco, Setpoint_Current, ConstC, LastCalc, Learn, Power

Yes, I will, it part of my to do list. This is the first time I publish my own code to the public and I’m not a ver good comments writer :wink:

You’re welcome and thank you for your questions

Thanks for your detailed answer!

I have the same setup! Thanks for clarifing this and all the other things also.

One thing I still can’t understand, I think you might misunderstood me, so I will try to rephrase my question :slight_smile:

I know that it works like a regular thermostat. My question was, that how this rule affects this? I mean that even if you don’t have any automation set up for heating, it will ‘calculate’ the needed power for heating, and will try to add the required heating to reach the setpoint. So what does this rule do? I couldn’t figure it out at a quick glance at the code. It will turn just turn ON/OFF the valve or it will alternate the target temperature?

Good question! A normal wall-thermostat works with a PID, it calculates based on experience when to turn ON/OFF the boiler. Newer systems with modulation are more advanced, they predict, based on history, when en how much the boiler needs to work (changing the water temperature).

The rule works a little like the ON/OFF thermostat. Based on history (it needs a few days to calibrate), it can predict how long the boiler needs to be ON to get the right room temperature. This is very different per room, per season, per isolation of the house, etc etc. Every 30 minutes (or whatever the CalcTime is set to) it recalculates.
Example: If the room temperature is 20 degrees, we want it to 21.5 degrees, the first run (let’s assume there is no calibration done yet, so the ConstC =60), the rule will let the boiler be ON for 27 minutes (1.5 * 60 * 30 (calctime)/100). After 27 minutes the boiler is OFF, after 30 minutes there is a recalibration -> the room temperature is 21.3degrees, according to the formula, the new ConstC will be 69 (instead of 60). So the rule will let the boiler run for 4 minutes (0.2 * 69 *30). And so on.
In the beginning there’ll be over- and undershoots, but eventually it will be around the right temperature.
It is based on this VERA plugin: Smart Virtual Thermostat

Thanks!

Now I can understand it better.
However I don’t have a ‘boiler’. I live in flat and it has it’s central-heating (boiler), so I don’t have one individually. Can I somehow use this rule that it turns off the individual thermostat valves itself and not the ‘boiler’?

Yes you can. The item XXX_XXX_BoilerValue is the key. It can be used as a virtual item or as a linked item. The rule uses this item as a “Heating needed”. So it can check if that device/room needs heat. In addition I’ve a second rule that watches the group with these items and if one of them becomes ON, it will start the boiler.
In your case, if you link it a valve (ex under de floor heating), the hot water from your central-heating will flow.
For the radiators with Spirits you can use it as a virtual item. The item XXX_XXX_Mode needs to link to the Spirit mode. Then based on the measured temperature, the Spirit will controle the valve for optimal heating. The rule will adapt to that behaviour.