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