Turn my central heating system into a zone-heating system: every room can set their own temperature. There are some proprietary systems, but I think these are too expensive and require me to replace almost my entire heating system. I think I can do it cheaper myself. This is a work in progress. I may describe the entire project in more detail at some later time, but for now I just want to share my idea and the dzvents script.
The idea is that I have some Eurotronic SpiritZ radiator valves and these control the heating per room. The SpiritZ however only opens and closes the radiator valve, the boiler still needs to be switched on to actually provide hot water to the radiator. Typically this is done by a thermostat in the living room. If it gets cold(er) in the living room, the boiler is switched on and hot water starts flowing. As long as the living room is warm enough however, also the other rooms will get no hot water and thus will remain cold. My challenge was to think of a way to make the boiler provide hot water when one of the rooms needs it. I currently only have a SpiritZ in the bathroom and another in the kitchen, but I am planning of putting one in every room (*).
I use SVT to control my boiler. SVT needs one ore more temperature sensors, plus one or more switch devices that it can use to switch the boiler on and off. The SVT plugin also adds a temperature setpoint device to Domoticz that can be used to set the required temperature. Basically what SVT does is switch on the boiler at time intervals to make sure the temperature returned by the temperature sensors is close to the temperature set in the setpoint device. I use a cheap wifi relay (1,79 euro) as the switch to control my boiler.
Now what I need to do is:
1 - make sure SVT's setpoint is set so that every room can be heated up to the temperature set in the SpiritZ for that room and
2 - make sure SVT knows if the temperature is at or close to the temperatures set in the SpiritZ valves.
3 - switch off the boiler if no SpiritZ requires hot water.
I made SVT read the temperature from a dummy temperature sensor that gets updated using my script. Plus I make my script update SVT's setpoint. To calculate both the setpoint and the temperature for SVT I use the valve opening reported by each SpiritZ.
The setpoint is easiest: SVT's setpoint is set to the highest setpoint of all SpiritZ's that have a valve opening higher than 0%. This way SVT will always deliver water hot enough to heat up to the highest requested temperature. A room that is already at it's temperature set in the SpiritZ will not be heated higher than needed because the valve will be closed for that room. My family will only change the SpiritZ's setpoints, SVT's setpoint is calculated from those.
The measured temperature is a bit more complicated: I calculate the average of all room's temperatures, but I assign a weight to each of those values: the opening percentage of the valve in that room. If the difference between the room's setpoint and the actual temperature is high, the valve will be wide open (100%), if the room temperature is close to the setpoint, the valve will be open only a little and if the room temperature is over the setpoint, the valve will closed, i.e. at 0%. So what I do is multiply the room temperature by the valve percentage, then calculate the sum for all rooms. This sum I then divide by the sum of all room's valve percentages (each ranging from 0 to 100). A room that is at or above it's setpoint will have 0, i.e. it's temperature is not used in the calculation for SVT's temperature, a room that is far below it's setpoint will heavily influence SVT's temperature, a room will have less influence on SVT's temperature the closer it comes to it's setpoint, until the valve closes and it doesn't influence the temperature at all.
I also added a correction on the valve opening percentage to compensate for a valve that doesn't completely open (100%) or close (0%) due to physical limitations. But that wasn't needed at all, as the SpiritZ apparently automatically calibrates the valve opening to go all the way from 0% to 100%, both my installed valves go to both extremes.
I am going to clean the script up later. Probably going to do a full rewrite once I've got more valves installed (I don't have the hardware installed yet) and am past this initial phase of experimenting, but until the I still want to share this preliminary script with you all:
Code: Select all
local KEUKEN_VALVE_NAME = "Keuken: Radiator: Valve Opening"
local KEUKEN_SENSOR_NAME = "Keuken: Radiator: Air Temperature"
local KEUKEN_SENSOR2_NAME = "Keuken: TempHum"
local KEUKEN_SETPOINT_NAME = "Keuken: Radiator: Heat"
local BADKAMER_VALVE_NAME = "Badkamer: Radiator: Valve Opening"
local BADKAMER_SENSOR_NAME = "Badkamer: Radiator: Air Temperature"
local BADKAMER_SENSOR2_NAME = "Badkamer: TempHum"
local BADKAMER_SETPOINT_NAME = "Badkamer: Radiator: Heat"
local WOONKAMER_SENSOR_NAME = "Woonkamer: TempHum"
local WOONKAMER_SETPOINT_NAME = "Woonkamer: Verwarming"
local VERWARMING_SETPOINT_NAME = "Verwarming"
local VERWARMING_TEMPERATURE_NAME = "Verwarming: Temperature"
return {
on = {
devices = {
BADKAMER_VALVE_NAME,
KEUKEN_VALVE_NAME,
BADKAMER_SENSOR_NAME,
BADKAMER_SENSOR2_NAME,
KEUKEN_SENSOR_NAME,
KEUKEN_SENSOR2_NAME,
WOONKAMER_SENSOR_NAME,
BADKAMER_SETPOINT_NAME,
KEUKEN_SETPOINT_NAME,
WOONKAMER_SETPOINT_NAME
}
},
data = {
minimum_opening = { initial = {} },
maximum_opening = { initial = {} }
},
execute = function(domoticz, device)
if (true == device.isDevice) then
domoticz.log('Device ' .. device.name, domoticz.LOG_ERROR)
local keuken_valve = domoticz.devices(KEUKEN_VALVE_NAME)
local keuken_sensor = domoticz.devices(KEUKEN_SENSOR_NAME)
local keuken_sensor2 = domoticz.devices(KEUKEN_SENSOR2_NAME)
local keuken_setpoint = domoticz.devices(KEUKEN_SETPOINT_NAME)
local badkamer_valve = domoticz.devices(BADKAMER_VALVE_NAME)
local badkamer_sensor = domoticz.devices(BADKAMER_SENSOR_NAME)
local badkamer_sensor2 = domoticz.devices(BADKAMER_SENSOR2_NAME)
local badkamer_setpoint = domoticz.devices(BADKAMER_SETPOINT_NAME)
local woonkamer_sensor = domoticz.devices(WOONKAMER_SENSOR_NAME)
local woonkamer_setpoint = domoticz.devices(WOONKAMER_SETPOINT_NAME)
local verwarming_setpoint = domoticz.devices(VERWARMING_SETPOINT_NAME)
local verwarming_temperature = domoticz.devices(VERWARMING_TEMPERATURE_NAME)
-- Depending on the way the valve got installed and the valve foot on the radiator, the valve may
-- close well above 0% and thus never reach that 0%. I want a range of 0 to 100% though to calculate
-- a proper weighted average temperature. So for this reason I keep track of the lowest opening
-- level ever reached for each valve, so I can do a correction on the opening level further
-- below. The same may be true for the maximum level, but I'm going to ignore that for now.
--
-- Instruction to get the minimum value set fastest: Put the setpoint for each valve at an extremely
-- low temperature -i.e. well below the current room temperature- and leave it there for a few minutes so
-- that the valve will close. The script will now 'learn' the lowest opening value this valve can reach.
if (domoticz.data.minimum_opening[KEUKEN_VALVE_NAME] == nil or keuken_valve.level < domoticz.data.minimum_opening[KEUKEN_VALVE_NAME]) then
domoticz.data.minimum_opening[KEUKEN_VALVE_NAME] = keuken_valve.level
end
if (domoticz.data.maximum_opening[KEUKEN_VALVE_NAME] == nil or keuken_valve.level > domoticz.data.maximum_opening[KEUKEN_VALVE_NAME]) then
domoticz.data.maximum_opening[KEUKEN_VALVE_NAME] = keuken_valve.level
end
if (domoticz.data.minimum_opening[BADKAMER_VALVE_NAME] == nil or badkamer_valve.level < domoticz.data.minimum_opening[BADKAMER_VALVE_NAME]) then
domoticz.data.minimum_opening[BADKAMER_VALVE_NAME] = badkamer_valve.level
end
if (domoticz.data.maximum_opening[BADKAMER_VALVE_NAME] == nil or badkamer_valve.level > domoticz.data.maximum_opening[BADKAMER_VALVE_NAME]) then
domoticz.data.maximum_opening[BADKAMER_VALVE_NAME] = badkamer_valve.level
end
-- Determine the corrected weight-factor for each valve.
-- TODO: Add correction so that the actual opening range is from "minimum opening" to 100% again.
local weight = {}
if (keuken_valve.level < domoticz.data.minimum_opening[KEUKEN_VALVE_NAME] or domoticz.data.maximum_opening[KEUKEN_VALVE_NAME] <= domoticz.data.minimum_opening[KEUKEN_VALVE_NAME]) then
weight[KEUKEN_VALVE_NAME] = 0.0
else
weight[KEUKEN_VALVE_NAME] = 100.0 * ((keuken_valve.level - domoticz.data.minimum_opening[KEUKEN_VALVE_NAME]) / (domoticz.data.maximum_opening[KEUKEN_VALVE_NAME] - domoticz.data.minimum_opening[KEUKEN_VALVE_NAME]))
end
if (badkamer_valve.level < domoticz.data.minimum_opening[BADKAMER_VALVE_NAME] or domoticz.data.maximum_opening[BADKAMER_VALVE_NAME] <= domoticz.data.minimum_opening[BADKAMER_VALVE_NAME]) then
weight[BADKAMER_VALVE_NAME] = 0.0
else
weight[BADKAMER_VALVE_NAME] = 100.0 * ((badkamer_valve.level - domoticz.data.minimum_opening[BADKAMER_VALVE_NAME]) / (domoticz.data.maximum_opening[BADKAMER_VALVE_NAME] - domoticz.data.minimum_opening[BADKAMER_VALVE_NAME]))
end
-- Determine the highest setpoint that has a non-zero weight.
local setpoints = { woonkamer_setpoint.setPoint } -- woonkamer doesn't have a valve, it is always fully open (i.e. at 100%).
-- This safes me from installing a bypass for now :-).
if weight[KEUKEN_VALVE_NAME] > 0 then
table.insert( setpoints, keuken_setpoint.setPoint)
end
if weight[BADKAMER_VALVE_NAME] > 0 then
table.insert( setpoints, badkamer_setpoint.setPoint)
end
local setpoint_calculated = nil
for i, sp in ipairs( setpoints ) do
if (setpoint_calculated == nil or sp > setpoint_calculated) then
setpoint_calculated = sp
end
end
-- Now determine the weighted average of all temperature sensors
local temperature_total = 100 * woonkamer_sensor.temperature
+ weight[KEUKEN_VALVE_NAME] * keuken_sensor.temperature
+ weight[KEUKEN_VALVE_NAME] * keuken_sensor2.temperature
+ weight[BADKAMER_VALVE_NAME] * badkamer_sensor.temperature
+ weight[BADKAMER_VALVE_NAME] * badkamer_sensor2.temperature
local weight_total = (100 + weight[KEUKEN_VALVE_NAME] + weight[KEUKEN_VALVE_NAME] + weight[BADKAMER_VALVE_NAME] + weight[BADKAMER_VALVE_NAME])
local weighted_temperature = math.floor((temperature_total / weight_total) * 10 + 0.5) / 10
domoticz.log('Calculated setpoint: ' .. tostring(setpoint_calculated) .. ', Weighted temperature: ' .. tostring(weighted_temperature) .. ', weights: bk: ' .. weight[BADKAMER_VALVE_NAME] .. ', k: ' .. weight[KEUKEN_VALVE_NAME] .. '.', domoticz.LOG_ERROR)
if (verwarming_setpoint.setPoint ~= setpoint_calculated) then
domoticz.log('Updating setpoint: ' .. tostring(setpoint_calculated).. '.', domoticz.LOG_ERROR)
verwarming_setpoint.updateSetPoint(setpoint_calculated)
end
if (verwarming_temperature.temperature ~= weighted_temperature) then
domoticz.log('Updating temparature: ' .. tostring(weighted_temperature).. '.', domoticz.LOG_ERROR)
verwarming_temperature.updateTemperature(weighted_temperature)
end
end
end
}
(*) You have to be careful when you change an existing 'normal' heating system into a zone-heating system. because if all rooms are warm enough, all valves will be closed but the pump will still be running. Either your pump will break or your pipes will start leaking. Plus the boiler can not do its work efficiently if the water flowing back into the boiler is not a couple degrees cooler than the water flowing away from it. Modern HR boilers will even start giving errors if the return water is not cooler. To make sure this doesn't happen, you must either always leave one radiator open, or have a bypass valve built into your system (a bypass is an automatic valve that opens automatically if the pressure goes over some set pre-set value, thereby making sure a loop remains open when all radiators are closed) https://www.heatnet.nl/bypass.html.