Je domoticz depuis janvier avec un raspberry B+ et j’ai planché sur un contrôle de température à base de PID. Je voulais vraiment pouvoir piloter à distance la température : genre couper le weekend sans qu’il fasse 13°C lorsqu’on rentre le dimanche soir.
Mon installation se compose d’une chaudière à gaz avec relais de contrôle, d’une sonde de température intérieure dans le salon, d’un RFXtrx433e et une piface en guise de carte I/O.
Principe de base :
Un script_device suit l’évolution de la température de la sonde oregon par rapport à la consigne et détermine une consigne de chauffage de 0 à 100 en fonction :
1 : de l’écart avec la consigne (Proportionnel) jusqu’ici tout la bien
2 : de la somme des écarts (Intégral) ça ce n’est pas trop dur non plus
3 : de la variation de l’écart (Dérivée) la ça se corse
Commande de chauffage = Kp*Erreur + Ki*Somme des erreurs + Kd*Delta de l’erreur
Les variables Kp, Ki et Kd sont fonction de chaque installation.
Code : Tout sélectionner
-- script_device_thermostatv3.lua ------------------------------------
----------------------------------------------------------------------
-- G PETREMENT 2015 --------------------------------------------------
-- This script is a heating regulation use to maintain temperatur ----
-- at a specific value. ----------------------------------------------
----------------------------------------------------------------------
--------------------------------
--- Variables utilisateur ------
--------------------------------
local VarConsigne = "ConsigneThermostat" --Température de consigne
local sonde = 'Sonde salon' --Nom de la sonde de température
local IDerreur = '62' --IDX de l'afficheur d'erreur
local IDSomerreur = '77' --IDX de l'afficheur Somme d'erreur
local IDDeltaErreur = '90' --IDX de l'afficheur Différence d'erreur
local IDCmdChauff = '78' --IDX de l'afficheur Proportionnel Commande Chauffage
local domoticz_url = '192.168.0.4:8080' --Adresse IP:port de Domoticz
local VarKp = "Kp" --Variable Domoticz : Gain proportinnel
local VarKi = "Ki" --Variable Domoticz : Gain intégral
local VarKd = "Kd" --Variable Domoticz : Gain dérivé
local VarTempErr = 'TemperatureErreur' --Variable domoticz pour l'erreur de temperature
local VarSomErr = 'SommeErreur' --Variable domoticz pour la somme des erreurs
local VarDeltaErr = 'DeltaErreur' --Variable domoticz pour la différences des erreurs
local VarCmdChauff = 'CmdChauff' --Variable Domoticz de commande du chauffage : permet de passer la commande à script_time_chauff.lua
local VarCmdImax = 'CmdImax' --Variable domoticz qui défini le gain intégral maximum
local fichier = '/var/tmp/dom_tmperr.txt' --Chemin d'acces du fichier sur un RAMdisque pour réduire l'usure de la carte SD
local nbr_stk = 22 --Quantité de valeurs utilisées pour le calcul de la dérivée de l'erreur (ne pas mettre 0!!)
--------------------------------------------------------------
-- Fonction pour arrondir un nombre à la décimale souhaitée --
--------------------------------------------------------------
math.round = function(number, precision)
precision = precision or 0
local decimal = string.find(tostring(number), ".", nil, true);
if ( decimal ) then
local power = 10 ^ precision;
if ( number >= 0 ) then
number = math.floor(number * power + 0.5) / power;
else
number = math.ceil(number * power - 0.5) / power;
end
-- convert number to string for formatting
number = tostring(number);
-- set cutoff
local cutoff = number:sub(decimal + 1 + precision);
-- delete everything after the cutoff
number = number:gsub(cutoff, "");
else
-- number is an integer
if ( precision > 0 ) then
number = tostring(number);
number = number .. ".";
for i = 1,precision
do
number = number .. "0";
end
end
end
return number;
end
---------------------------------------------------------
--- Fonction pour calculer la commande de chauffage ---
---------------------------------------------------------
Chauff = function(Kp,Err,Ki,SomErr, Kd, DifErr)
local cmd = Kp*Err + Ki*SomErr + Kd*DifErr
-- La commande de chauffage est un nombre entier compris entre 0 et 100
if cmd < 0 then
cmd = 0
elseif cmd > 100 then
cmd = 100
else
cmd = math.round(cmd, 0)
end
return cmd
end
-----------------------------
---- Programme principal ----
-----------------------------
commandArray = {}
--La sonde Oregon 'Salon' emet toutes les 40 secondes. Ce sera approximativement la fréquence d'exécution de ce script.
if (devicechanged[sonde]) then
-- Initialisation des variables
local t = {} --Table buffer avec le fichier texte
local SomErreur = tonumber(uservariables[VarSomErr]) --Somme des erreurs
local Kp = tonumber(uservariables[VarKp])
local Ki = tonumber(uservariables[VarKi])
local Kd = tonumber(uservariables[VarKd])
local CmdImax = tonumber(uservariables[VarCmdImax]) --Limitation de l'action intégrale (debug)
local consigne = otherdevices_svalues[VarConsigne] --Température de consigne
---------------------------------
--- Calcul du proportionnel ---
---------------------------------
--Temperature relevée dans le salon
local temperature = devicechanged[sonde..'_Temperature']
temperature = tonumber(math.round(temperature,2))
-- Update de la sonde d'erreur
local TmpErreur = consigne - temperature
local url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDerreur..'&nvalue=0&svalue='..TmpErreur
commandArray[1]={['OpenURL']=url }
------------------------------
--- Calcul de la dérivée ---
------------------------------
--Creation si le ficher n'existe pas
local f = io.open(fichier, "a+")
f:close()
--Lecture du fichier et remplissage de la table t
f = io.open(fichier, "r")
for line in f:lines() do
if line ~= nil then
table.insert(t,line)
end
end
f:close()
-- Ajout de la nouvelle ligne a la table
table.insert(t,tostring(os.time()..";"..math.round(TmpErreur,2)))
--Suppression de l'élement le plus vieu
while #t > nbr_stk do
table.remove(t,1)
end
-- Eriture dans fichier'
f = io.open(fichier,"w")
for key,value in pairs(t) do
f:write(value.."\n")
end
f:close()
-- Calcul Difference entre deux erreurs à 15min d'interval + calibration
local MoyErr1 = 0
local MoyErr2 = 0
for i=1,#t/2 do
MoyErr1 = MoyErr1 + tostring(string.sub(t[i],string.find(t[i], ";")+1))
MoyErr2 = MoyErr2 + tostring(string.sub(t[#t+1-i],string.find(t[#t+1-i], ";")+1))
end
--Le delta erreur est calculé sur la moyenne de 22 erreurs ce qui permet de 'lisser' la variation
local DeltaErreur = tonumber(math.round(((MoyErr2 / (#t/2)) - (MoyErr1 / (#t/2)))*8 ,2)) --Delta en d°/heure
print('--DeltaErreur = ('..MoyErr2..'/'..(#t/2)..') - ('..MoyErr1..'/'..(#t/2)..') * 8= '..DeltaErreur)
-- Update de la sonde DeltaErreur
url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDDeltaErreur..'&nvalue=0&svalue='..DeltaErreur
commandArray[2]={['OpenURL']=url }
-------------------------------
--- Calcul de l'intégrale ---
-------------------------------
-- Calcul de la somme des erreurs + Update de la sonde Somme d'erreur
-- Si la l'erreur est supérieure à +-1° on ne somme pas pour limiter l'impact des changements de consigne
if ((TmpErreur > -1) and (TmpErreur < 1) and (Ki ~= 0)) then
SomErreur = tonumber(math.round(TmpErreur + SomErreur,2))
if (SomErreur > (CmdImax/Ki)) then
SomErreur = CmdImax/Ki
elseif (SomErreur < 0) then
SomErreur = 0
end
end
url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDSomerreur..'&nvalue=0&svalue='..SomErreur
commandArray[3]={['OpenURL']=url }
------------------------------------------
--- Calcul de la commande de chauffage ---
------------------------------------------
print('Consigne = '..consigne..' Temperature = '..temperature)
local CmdChauff = Chauff(Kp, TmpErreur, Ki, SomErreur, Kd, DeltaErreur)
-- Update de la mesure Commande
url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDCmdChauff..'&nvalue=0&svalue='..CmdChauff
commandArray[4]={['OpenURL']=url }
print('--Commande de chauffage : '..CmdChauff..' / Kp ='..tostring(Kp*TmpErreur)..' / Ki ='..tostring(Ki*SomErreur)..' / Kd ='..tostring(Kd*DeltaErreur))
--Passage des variables à domoticz
commandArray['Variable:'..VarTempErr]= tostring(TmpErreur)
commandArray['Variable:'..VarSomErr]= tostring(SomErreur)
commandArray['Variable:'..VarDeltaErr]= tostring(DeltaErreur)
commandArray['Variable:'..VarCmdChauff]= tostring(CmdChauff)
end
return commandArray
C’est un genre de modulation PWM avec une base de temps assez longue. C’est un peu obligé pour éviter l’usure de la chaudière, on ne peut pas démarrer et arrêter le bruleur tous les 10s, ce n’est pas une résistance électrique. En plus j’ai ajouté deux protections, une pour éviter les cycles trop courts (ça de toute façon il doit y avoir une protection matérielle sur la chaudière mais je n’ai pas trouvé d’info mais je suis preneur si certains connaisse) dans mon cas on évitera les cycles de chauffe inférieur à 3min, et une deuxième pour éviter les temps d’arrêt très court à la fin des cycles de presque 15min.
Code : Tout sélectionner
-- script_time_cmdchauffv2.lua ---------------------------------------
-- G PETREMENT -------------------------------------------------------
----------------------------------------------------------------------
-- Permet de commander relais de pilotage de chaudiere à gaz ---------
-- Le cycle fait 900s / 15min ---------------------------------------
-- La commande est un nombre de 0 à 100 ------------------------------
-- Le cycle minimum de la chaudière est de 3min pour réduire l'usure -
----------------------------------------------------------------------
local InterOnOff = 'Chauffage On/Off' --Interrupteur général de mise en route du chauffage
local VarCmdChauff = 'CmdChauff' --Variable de commande de chauffage définie par script_device_thermostat
local CycleMini = 180 --Durée de chauffe minimum en secondes (fonction de la chaudière)
local InterRelais = 'Piface RelayChaud' --Sortie vers relais chaudière
local domoticz_url = '192.168.0.4:8080' --Adresse IP:port de Domoticz
-- Fonction retourant la differrence de temps en secondes
function timedifference(s)
year = string.sub(s, 1, 4)
month = string.sub(s, 6, 7)
day = string.sub(s, 9, 10)
hour = string.sub(s, 12, 13)
minutes = string.sub(s, 15, 16)
seconds = string.sub(s, 18, 19)
t1 = os.time()
t2 = os.time{year=year, month=month, day=day, hour=hour, min=minutes, sec=seconds}
difference = os.difftime (t1, t2)
return difference
end
commandArray = {}
-- Recuperation des variables
local CmdChauff = uservariables[VarCmdChauff]
local OnOff = otherdevices[InterOnOff]
-- Recuperation des minutes
time=os.time()
minutes=tonumber(os.date('%M',time))
hours=tonumber(os.date('%H',time))
-- Execution de cette partie du script toutes les 15 minutes
-- Contacte la chaudière et modifie la variable domoticz Cycle
if( (minutes==15) or (minutes==0) or (minutes==30) or (minutes==45) ) then
print('-- Lancement du script à '..hours..'h'..minutes)
print('-- OnOff ='..OnOff..' / CmdChauff ='..CmdChauff)
if OnOff == 'On' then --Si l'inter général est ON
local Cycle = CmdChauff * 9 --Calcul du temps de cycle en seconde
if Cycle < CycleMini then --Si le cycle est trop court on coupe
commandArray[InterRelais]='Off'
commandArray['Variable:'..VarCycle]= tostring('0')
print('Cycle :'..Cycle..'s >> Chaudière Off')
elseif Cycle > 870 then --Si le cycle est trop long (> 14min30) on allume sans coupure
commandArray[InterRelais]='On'
commandArray['Variable:'..VarCycle]= tostring(Cycle)
print('Cycle :'..Cycle..'s >> Chaudière On')
else --Enfin on lance pour X seconde
print('Cycle lancé pour : '..Cycle..'secondes')
commandArray[1]={[InterRelais]='On'}
commandArray[2]={[InterRelais]='Off AFTER '..tostring(Cycle)}
end
else
commandArray[InterRelais]='Off'
commandArray['Variable:'..VarCycle]= tostring('0')
print('Chaudière Off')
end
end
return commandArray
Le premier est un bouton programmé. Quand c’est ON, la consigne passe à la valeur « Confort » (stocké en variable), quand c’est OFF on passe en Eco. On peut aussi agir sur le bouton manuellement. Un bouton HorsGel qui désactive le bouton programmé et passe la température en mode « Pas La ». On peut toujours agir sur la consigne en manuel et à tout moment.
Code : Tout sélectionner
-- script_device_conschauf.lua ---------------------------------------
-- G PETREMENT -----------------------------------------------2015----
----------------------------------------------------------------------
-- Pilotage d'un Thermostat setpoint Consigne pour pilotage regul ----
-- en fonction d'une programation journaliere ------------------------
----------------------------------------------------------------------
-- Le programme est basé sur l'action de 2 switchs (virtuels ou non) -
-- Le Switch EcoConfort dispose d'une programation mais peut aussi ---
-- être actionné manuellement. ---------------------------------------
-- Le switch HorsGel est un mode vacance qui stope la programmation --
-- Les temperatures de consignes sont stockées en variable direct ----
-- dans domoticz pour être modifiées par l'interface web. ------------
----------------------------------------------------------------------
----------------------------------------------------------------------
-- VARIABLES UTILISATEUR : mettre les nom des dispositifs ici --------
----------------------------------------------------------------------
local VarConsJour = 'ConsigneJour' --Variable consigne jour
local VarConsNuit = 'ConsigneNuit' --Variable consigne nuit
local VarConsVac = 'ConsigneVacance' --Variable consigne vacances
local VarSomErr = 'SommeErreur' --Variable Somme des erreurs
local SondeCons = 'ConsigneChauffage' --Sonde de temperature
local InterEcoConfort = 'EcoConfort' --Interrupteur programmé Off:eco On:confort
local InterHorsGel = 'HorsGel' --Interrupteur On:mode hors gel>> plus de programmation, consigne vacance ou manuelle
local domoticz_url = '192.168.0.4:8080' --Adresse IP:port de Domoticz
local IDXcons = '96' --IDx de l'afficheur de consigne
----------------------------------------------------------------------
-- Fonction retourant la differrence de temps en secondes ------------
function timedifference(s)
year = string.sub(s, 1, 4)
month = string.sub(s, 6, 7)
day = string.sub(s, 9, 10)
hour = string.sub(s, 12, 13)
minutes = string.sub(s, 15, 16)
seconds = string.sub(s, 18, 19)
t1 = os.time()
t2 = os.time{year=year, month=month, day=day, hour=hour, min=minutes, sec=seconds}
difference = os.difftime (t1, t2)
return difference
end
commandArray = {}
-- Recuperation des variables ---------------------------------------
local ConsJour = uservariables[VarConsJour]
local ConsNuit = uservariables[VarConsNuit]
local ConsVac = uservariables[VarConsVac]
local Consigne = otherdevices_temperature[SondeCons]
local t1 = os.date("%H:%M")
-- Lorsqu'un interrupteur est manipulé on modifie la temperature de consigne.
-- La variable SommeErreur utilisée pour calculer le gain intégral est aussi remise à 0 >> Gain intégral=0
if (((devicechanged[InterEcoConfort] == 'On') and (otherdevices[InterHorsGel] == 'Off')) or ((devicechanged[InterHorsGel] == 'Off') and (otherdevices[InterEcoConfort] == 'On'))) then
print("-------------- Temperature modifiée vers Confort à "..t1)
-- Mise a jour de l'afficheur via JSON -----------------------------
local url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDXcons..'&nvalue=0&svalue='..ConsJour
commandArray[1]={['OpenURL']=url }
commandArray['Variable:'..VarSomErr]='0'
elseif (((devicechanged[InterEcoConfort] == 'Off') and (otherdevices[InterHorsGel] == 'Off')) or ((devicechanged[InterHorsGel] == 'Off') and (otherdevices[InterEcoConfort]=='Off' ))) then
print("-------------- Temperature modifiée vers Eco à "..t1)
-- Mise a jour de l'afficheur via JSON -----------------------------
local url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDXcons..'&nvalue=0&svalue='..ConsNuit
commandArray[1]={['OpenURL']=url }
commandArray['Variable:'..VarSomErr]='0'
elseif (devicechanged[InterHorsGel] == 'On') then
print("-------------- Temperature modifiée vers HorsGel à "..t1)
-- Mise a jour de l'afficheur via JSON -----------------------------
local url = domoticz_url..'/json.htm?type=command¶m=udevice&idx='..IDXcons..'&nvalue=0&svalue='..ConsVac
commandArray[1]={['OpenURL']=url }
commandArray['Variable:'..VarSomErr]='0'
end
return commandArray
Variables domoticz :
J'utilise les variable pour deux choses : transmettre des valeurs entre deux scripts et choisir les constantes des scripts (ce qui évite d'aller bidouiller dans nano) Résultats du système :
J’ai implémenté le coef Kp en premier. Déjà là avec un gain suffisant on arrive à avoir une erreur de 0,3°C seulement et un réel confort de chauffage. Contrairement aux systèmes «hystérésis» ou on lutte contre l’inertie de la maison (la mienne est en pierre ce qui joue pas mal), là on allume la chaudière quelque minutes tous les ¼ d’heure ce qui permet de maintenir une sensation de chaud agréable.
L’ajout du Ki permet de réduire l’écart résiduel. La cmd intégrale va augmenter progressivement (on somme l’écart avec les écarts précédants) puis se stabiliser lorsque la consigne est atteinte (écart = 0). Par contre on augmente l’instabilité du système cad une ondulation de la température sur 1/2heure ou 1 heure. C’est un peu gênant et oblige à choisir un Ki pas trop fort au détriment du temps d’obtention de la consigne (effet retardateur). J’ai ajouté des limites à l’action du Ki car cette action peut vraiment mettre le cayon surtout si le système commence à onduler méchamment avec une amplitude de l’ordre du degré. Donc j’ai limité l’action du Ki à 60% de la commande de chauffage, je remets le Ki à 0 à chaque changement programmé de température (surtout utile durant la période de réglage, j’ai vu des Gain intégrale à 3 ou 4 zéro !!) enfin la somme des erreurs ne se fait que si on est dans un intervalle de 1° autour de la consigne. Je pense qu’une fois équilibrée ces limites doivent pouvoir dégager sans risque mais comme ça ne nuit pas…
Le Kd « stabilise » le système. Je l’ai finalement ajouté car j’avais des problèmes de dépassement du point de consigne. Soit à la chauffe avec un dépassement positif ou au refroidissement avec un dépassement négatif. Le Kd s’oppose à la variation de température donc lorsque les deux autres termes sont faibles il ralentit la courbe de montée ou de décente. La commande dérivée a été le plus complexe à calculer car l’écart entre deux températures successives est nul la plupart du temps (précision à 0,1). J’ai donc fait un calcul basé sur la moyenne des 22 dernières mesures (fichier texte stocké sur RAMdisk). Du coup le deltaT à un peu de « retard » mais ça à l’air de marcher.
Et maintenant ?
Maintenant ça marche tout seul, le dépassement de température est vraiment minime (0,2° max) le confort est la et on « vois » bien le système moduler en fonction de la température extérieure. Je pense qu’il faudrait refaire un réglage des coef en suivant une méthode de calcul mais je n’ai pas vraiment le temps. De plus je me demande si ça fonctionnera toujours aussi bien lorsqu’il fera moins froid.
Pour le moment la programmation est super simple mais on peut imaginer greffer un GoogleCal là-dessus pour personnaliser au maximum.
Je pense aussi faire un afficheur bien design à base de matrice de led mais j’ai pas encore défini le cahier des charges histoire de pouvoir faire un réglage simple sans allumer l’ipad.
Limitations du système
Le premier danger c’est qu’est qui se passe si le bazard plante ? Coupure d’élec, usure SD, relais HS, sonde morte… Une analyse des risques rapide montre que le danger existe. La chaudière possède des sécurités qui devraient limiter le risque d’incendie. Par contre de la surconsommation avec une baraque à 35°C pendant 10jours de vacance c’est possible. Ou pire, chauffage à l’arrêt, maison à -5 et tous les tuyaux qui éclatent. Il faut donc mettre des sécurités à tous les niveaux :
En premier lieu je pense modifier le programme pour qu’il ne dépende pas complétement d’une seule sonde. Je vais intégrer la possibilité utiliser d’autres sondes en cascades (en y appliquant des offsets si besoin) voir même on peut utiliser la température du core du raspi si celui-ci est dans une des pièces chauffées (le delta est constant avec la température ambiante, ça pourrai servir de sécurité supplémentaire).
Ensuite il faudrait intégrer un script (python, shell…) pour le cas ou domoticz planterai catastrophiquement et ne se relance pas.
Pour finir je pense mettre un système de sécurité externe (genre vieux thermostat d’ambiance) qui pourrai prendre la main lorsque le raspi est éteint (utilisation du relais 2 du piface en position NF). Je pourrais utiliser aussi un script sur le NAS pour vérifier le bon fonctionnement de domoticz… à voir.