Hello everyone,
I hope you're all well. I'm writing to you because I'm having a problem with a script related to a PI controller for a radiant floor heating system. The system consists of a 0/10 valve and a flow sensor. To prevent the controller from becoming too sensitive to temperature variations, a moving average filter was added. However, despite this precaution, the PID remains very lazy and unstable.
I've attached the script and an image of the configured parameters. The setpoints, proportional, and integral values are adjustable via the supervisory system. I feel like I'm a bit lost in the dark, so I'm counting on your expert help to resolve this situation. I forgot about the Pt1000 sensors read by the KNX module with a 2-minute cyclic variation and a value change of 0.2K, but the valves are 0/10 controlled by the KNX module.
Thank you so much for your support and availability.
Michael
I hope you're all well. I'm writing to you because I'm having a problem with a script related to a PI controller for a radiant floor heating system. The system consists of a 0/10 valve and a flow sensor. To prevent the controller from becoming too sensitive to temperature variations, a moving average filter was added. However, despite this precaution, the PID remains very lazy and unstable.
I've attached the script and an image of the configured parameters. The setpoints, proportional, and integral values are adjustable via the supervisory system. I feel like I'm a bit lost in the dark, so I'm counting on your expert help to resolve this situation. I forgot about the Pt1000 sensors read by the KNX module with a 2-minute cyclic variation and a value change of 0.2K, but the valves are 0/10 controlled by the KNX module.
Thank you so much for your support and availability.
Michael
Code:
-- ============================================================
-- REGOLATORE PI – MISCELATRICE PAVIMENTO RADIANTE
-- SOLO SONDA MANDATA
-- ============================================================
---------------------------------------------------------------
-- PARAMETRI BASE (persistenti)
---------------------------------------------------------------
local Kp = storage.get('Kp_PINT') or 3.0
local Ki = storage.get('Ki_PINT') or 0.02
local integral = storage.get('integral_PINT') or 0
local last_mode = storage.get('last_mode')
---------------------------------------------------------------
-- CONFIGURAZIONE KNX
---------------------------------------------------------------
local group_mode = '5/0/0' -- 1=inverno / 0=estate
local group_in = '5/2/1' -- temperatura mandata
local group_out = '5/0/1' -- 0–10 V miscelatrice
local group_set_winter = '5/2/4'
local group_set_summer = '5/2/5'
local group_kp = '5/2/6'
local group_ki = '5/2/7'
---------------------------------------------------------------
-- COSTANTI DI CONTROLLO
---------------------------------------------------------------
local cycle_time = 120 -- secondi
local filter_samples = 3 -- filtro ridotto
local out_min, out_max = 0, 100 -- %
local max_delta = 2 -- base rampa
local deadband = 0.3 -- °C
---------------------------------------------------------------
-- VARIABILI
---------------------------------------------------------------
local readings = {}
local last_output = storage.get('last_output') or 0
---------------------------------------------------------------
-- MEDIA MOBILE TEMPERATURA MANDATA
---------------------------------------------------------------
local function moving_average(v)
if type(v) ~= 'number' then return nil end
table.insert(readings, v)
if #readings > filter_samples then
table.remove(readings, 1)
end
local s = 0
for _, x in ipairs(readings) do
s = s + x
end
return s / #readings
end
---------------------------------------------------------------
-- CICLO PRINCIPALE
---------------------------------------------------------------
while true do
-------------------------------------------------------------
-- Aggiornamento Kp / Ki da KNX
-------------------------------------------------------------
local v = grp.getvalue(group_kp)
if type(v) == 'number' then
Kp = v
storage.set('Kp_PINT', Kp)
end
v = grp.getvalue(group_ki)
if type(v) == 'number' then
Ki = v
storage.set('Ki_PINT', Ki)
end
-------------------------------------------------------------
-- Modalità estate / inverno
-------------------------------------------------------------
local is_winter = grp.getvalue(group_mode) and true or false
if last_mode ~= is_winter then
integral = 0
last_mode = is_winter
storage.set('last_mode', is_winter)
end
-------------------------------------------------------------
-- Setpoint
-------------------------------------------------------------
local set = is_winter
and grp.getvalue(group_set_winter)
or grp.getvalue(group_set_summer)
if type(set) ~= 'number' then
set = is_winter and 35 or 25
end
-------------------------------------------------------------
-- Temperatura mandata filtrata
-------------------------------------------------------------
local T = grp.getvalue(group_in)
local Tf = moving_average(T)
if Tf then
-----------------------------------------------------------
-- LIMITI TEMPERATURA MANDATA
-----------------------------------------------------------
local Tmax = set + 2.0
local Tstop = set + 3.0
-----------------------------------------------------------
-- ERRORE
-----------------------------------------------------------
local error = set - Tf
if math.abs(error) < deadband then
error = 0
end
-----------------------------------------------------------
-- INTEGRALE
-- (bloccato solo sopra Tmax)
-----------------------------------------------------------
if Tf <= Tmax then
integral = integral + error * Ki * cycle_time
end
-----------------------------------------------------------
-- USCITA PI BASE
-----------------------------------------------------------
local output = Kp * error + integral
-----------------------------------------------------------
-- AZIONE FORTE SOPRA SETPOINT
-----------------------------------------------------------
if Tf > set then
output = output - Kp * (Tf - set) * 3
end
-----------------------------------------------------------
-- AZIONE PIÙ FORTE SOPRA Tmax
-----------------------------------------------------------
if Tf > Tmax then
output = output - Kp * (Tf - Tmax) * 4
end
-----------------------------------------------------------
-- TAGLIO DI SICUREZZA
-----------------------------------------------------------
if Tf >= Tstop then
output = out_min
integral = 0
end
-----------------------------------------------------------
-- SATURAZIONE
-----------------------------------------------------------
if output > out_max then
output = out_max
elseif output < out_min then
output = out_min
end
-----------------------------------------------------------
-- RAMPA DINAMICA
-- apertura veloce, chiusura controllata
-----------------------------------------------------------
local ramp = max_delta
if error > 5 then
ramp = max_delta * 4
elseif error > 2 then
ramp = max_delta * 2
end
local delta = output - last_output
if delta > ramp then
output = last_output + ramp
elseif delta < -max_delta then
output = last_output - max_delta
end
-----------------------------------------------------------
-- SCRITTURA KNX
-----------------------------------------------------------
if math.abs(output - last_output) >= 0.5 then
grp.write(group_out, output)
last_output = output
storage.set('last_output', output)
storage.set('integral_PINT', integral)
log(string.format(
"[%s] SP %.1f T %.1f OUT %.1f%% ERR %.2f",
is_winter and "INV" or "EST",
set, Tf, output, error
))
end
end
sleep(cycle_time)
end