[Tuto] Fil pilote et régulation PID

Vous avez créé un script LUA dont vous êtes fier, un .sh génial, un programme Python hors du commun, un Tuto, c'est ici que vous pouvez les partager.
Chrominator
Messages : 1042
Inscription : 19 déc. 2015, 07:29
Localisation : France

[Tuto] Fil pilote et régulation PID

Message par Chrominator »

Certains d'entre vous m'ont demandé les scripts qui permettent à Domoticz de commander un radiateur électrique avec régulation PID.
Il ne s'agit ni plus ni moins que de l'adaptation à mon besoin de l'excellent travail de Guitoo.
Bien entendu je ne pourrais pas être tenu pour responsable de l'usage que vous ferez de ce qui est décrit ici.

Pré-requis :
Disposer d'un radiateur électrique avec fil pilote tel que décrit dans l'article de l'Atelier du Geek.
C'est un Interrupteur télécommandé qui gère le fil pilote.
Un radiateur sans fil pilote pourrait également être utilisé mais dans ce cas un interrupteur suffisamment puissant devra gérer l'allumage et l'extinction du radiateur.
La logique ON et OFF des scripts devra aussi être inversée (fil pilote alimenté en 220V = pas de chauffage).

Une sonde de température dans la pièce où se situe le radiateur électrique (pour respecter la consigne).

Principe :
Mon chauffage fonctionne grâce à une chaudière à gaz.
Cette chaudière est régulée grâce à un thermostat connecté QIVIVO.
C'est dans cet espace que sont définis les différents programmes (températures et horaires).

Image

Il y a une très grande pièce dans laquelle le radiateur lié à la chaudière n'est pas vraiment suffisant pour le volume à chauffer. :(
Plutôt que d'ajouter un radiateur et ses tuyaux dans cette pièce, j'ai opté pour un radiateur électrique de 1500W avec fil pilote.

Les scripts suivants servent à commander le fil pilote pour chauffer la pièce en fonction de la consigne QIVIVO du moment.
A noter le démarrage de ce radiateur électrique a lieu à 3 conditions :
  • Bouton d'arrêt général dans Domoticz sur ON (Interrupteur virtuel)
  • Porte d'entrée close
  • Porte menant au s/sol close
Pourquoi portes closes ? J'ai un chien qui sait ouvrir les portes :o, inutile de chauffer lorsque ça se produit...

Le premier est un script shell lancé par la crontab de root sur mon raspberry pi toutes les 30 mn.
Il va chercher la consigne du moment sur Internet dans mon espace QIVIVO, je ne veux pas solliciter ce serveur trop fréquemment inutilement. :roll:
S'agissant d'un radiateur d'appoint, la prise en compte d'un changement de consigne avec un retard de 30 mn est acceptable.
Si l'interrupteur d'arrêt général passe sur ON, alors ce script sera aussi lancé toutes les 5 mn par le script qui exécute la régulation PID.
Il doit être exécuté rapidement sans quoi Domoticz envoie un warning selon lequel l'exécution a duré plus de 10s.
2016-01-23 17:31:03.647 Error: EventSystem: Warning!, lua script /home/pi/domoticz/scripts/lua/script_device_thermostatv3.lua has been running for more than 10 seconds
C'est pour cela que l'accès à Internet passe par un proxy-cache.
Ca n'évite pas le timeout à tous les coups, mais dans mon cas ça ne mange pas de pain (le proxy sert à d'autres choses). :)
Ce n'est pas un pré-requis, et le script peut être modifié pour sortir vers Internet directement (supprimer tout ce qui fait référence au proxy).

Ce script met à jour un thermostat virtuel (consigne) et une sonde de température virtuelle (température mesurée par le thermostat QIVIVO).
La consigne est également mémorisée dans un fichier pour intercepter le changement de consigne dans un autre script (passage d'une consigne de nuit à une autre consigne).

Code : Tout sélectionner

#!/bin/bash
#################################################################################################
#
# Script recherchant la température de consigne du thermostat Qivivo
# Auteur : Chrominator
# Version 1.4
# Changes :
#           2015-10-24 : Initial version
#			2015-12-31 : Récupération de la température intérieure mesurée
#			2016-01-22 : Mise à la consigne chaudière de la tête thermostatique du radiateur dans le couloir
#			2016-01-24 : Test avant de mettre à jour si QIVIVO est en cours de maintenance
#							(Pas de température en retour)
#
# Principe :
# Connexion à la page "Votre espace" http://www.qivivo.com/login
# puis http://www.qivivo.com/login-check qui renvoie vers la page des réglages du thermostat
# Ensuite mise à jour des températures de consigne et mesurées dans Domoticz
#
#
#################################################################################################
username="pseudonyme de connexion QIVIVO"
password="mot de passe QIVIVO"
useragent="Mozilla/5.0 (Windows NT 10.0; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0"
domoticz_url="http://127.0.0.1:8080"
#
#	Utilisation du proxy pour éviter les problèmes de lenteur d'exécution
#
http_proxy="ip du proxy:port"
proxy_user="domoticz"
proxy_passwd="mot de passe"
#
#	Index des dispositifs de Domoticz
#
# Setpoint virtuel
TempIdx="129"
# Sonde virtuelle
CorridorIdx="141"
# Nom du fichier qui mémorise la consigne de température
filetcons="/var/tmp/knowtcons.dat"

#Initialisation du répertoire sur disque qui va stocker le cookie
set http_proxy=$http_proxy
if [ ! -d "/var/tmp/surf" ]; then mkdir /var/tmp/surf
fi
if test -f /var/tmp/surf/cookies.txt
  then
    rm /var/tmp/surf/cookies.txt
fi
#	Suppression éventuelle du fichier d'une précédente exécution.
if [ -f "/var/tmp/surf/.qivivo" ]; then rm /var/tmp/surf/.qivivo
fi
#
#	On se connecte à la page d'accueil
wget --post-data "_username=$username&_password=$password" --no-check-certificate --keep-session-cookies --save-cookies=/var/tmp/surf/cookies.txt --load-cookies=/var/tmp/surf/cookies.txt -U "$useragent" --referer="http://www.qivivo.com/login" --delete-after --quiet --proxy-user="$proxy_user" --proxy-password="$proxy_passwd" http://www.qivivo.com/login >/dev/null
#
#	On va chercher la page qui contient la consigne
wget --post-data "_username=$username&_password=$password" --no-check-certificate --keep-session-cookies --load-cookies=/var/tmp/surf/cookies.txt -U "$useragent" --referer="http://www.qivivo.com/login" --output-document="/var/tmp/surf/.qivivo" --quiet --proxy-user="$proxy_user" --proxy-password="$proxy_passwd" http://www.qivivo.com/login_check > /dev/null
#
# On cherche le litéral knowtcons dans le html reçu :
#     <span class="knowtcons" style="display:none">20.0</span>
#	On va compter les caractères précédents ce qu'on veut récupérer, pas rusé et ça marchera tant que la page ne sera pas modifiée.
#
temp=`cat /var/tmp/surf/.qivivo |grep 'knowtcons'`
if [ $? == 0 ]; then
	knowtcons=`expr substr "$temp" 50 4`
	echo "La température de consigne est de $knowtcons°C"
	echo $knowtcons > $filetcons
	#
	# On cherche maintenant la température à l'intérieur du type : 
	#             <span class="tin_color">20</span>.<span class="tin_dixieme">5°
	#
	temp=`cat /var/tmp/surf/.qivivo |grep 'tin_color'`
	temp_inside=`expr substr "$temp" 37 2`"."`expr substr "$temp" 73 1`
	curl  "$domoticz_url/json.htm?type=command&param=udevice&idx=$TempIdx&nvalue=0&svalue=$temp_inside"
	# Mise à jour de la consigne du radiateur
	curl  "$domoticz_url/json.htm?type=command&param=udevice&idx=$CorridorIdx&nvalue=0&svalue=$knowtcons"
	echo "La température intérieure est de $temp_inside°C"
else
	echo "Erreur lors de la recherche de la température"
fi


Le second script est celui qui gère la régulation PID elle-même, c'est celui de Guitoo avec quelques changements notamment pour prendre en compte la consigne QIVIVO stockée sur disque et effacer les écarts mémorisés jusqu'alors (qui deviennent non pertinents). :evil:

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 =  "Consigne QIVIVO"    --Température de consigne
local IDConsigne = '20'                   -- IDX de l'afficheur de consigne
local sonde = 'Thermometre Salon'         --Nom de la sonde de température
local InterOnOff = 'Chauffage On/Off'     --Interrupteur général de mise en route du chauffage
--local IDerreur = '41'                   --IDX de l'afficheur d'erreur
--local IDSomerreur = '42'                --IDX de l'afficheur Somme d'erreur
--local IDDeltaErreur = '43'              --IDX de l'afficheur Différence d'erreur
local IDCmdChauff = '48'                  --IDX de l'afficheur Proportionnel Commande Chauffage
local domoticz_url = '127.0.0.1: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 fcons = '/var/tmp/knowtcons.dat'    -- Chemin vers la nouvelle consigne
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
   print ('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' émet 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 existante

  -- Lecture de l'ancienne consigne
  local c = io.input(fcons)
    local consigne_nouvelle = c:read()
  c:close()
  if (consigne_nouvelle ~= nil ) then
    consigne_nouvelle = math.round(tonumber(consigne_nouvelle), 2)
    print('Ancienne consigne='..consigne, 'Nouvelle consigne='..consigne_nouvelle)
     if (consigne ~= consigne_nouvelle) then 
        print('Reinitialisation de la somme et suppression du fichier')
        commandArray['Variable:'..VarSomErr]='0'
        os.remove(fichier) -- Modif du 03/11/2015 pour éviter les valeurs énormes de Kd
        consigne = consigne_nouvelle
     end
     -- Update de la nouvelle consigne
     local url = domoticz_url..'/json.htm?type=command&param=udevice&idx='..IDConsigne..'&nvalue=0&svalue='..consigne_nouvelle
     commandArray[1]={['OpenURL']=url }
  end
 
  --  Recuperation des minutes
  time=os.time()
  minutes=tonumber(os.date('%M',time))
  hours=tonumber(os.date('%H',time))
  freq=tonumber(string.sub(os.date('%M',time), 2))

  -- Execution de cette partie du script toutes les 5 minutes si le chauffage est en fonction
  -- On limite de cette manière les accès inutiles au www Qivivo 

  local OnOff = otherdevices[InterOnOff]

  if( ((freq==5) or (freq==0)) and OnOff == 'On' ) then
     print('-- Récupération de la consigne QIVIVO à '..hours..'h'..minutes)
     os.execute('/home/pi/domoticz/scripts/get_tcons.sh')
  end

  ---------------------------------
  ---  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&param=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&param=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&param=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&param=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

Le dernier script est celui qui agit sur le fil pilote.
Il est issu de celui de Guitoo, avec quelques modifications.
Attention à la logique inverse due au fil pilote ;
  • Fil pilote alimenté en 220V = Arrêt du radiateur - pas de chauffe
  • Fil pilote non alimenté = Démarrage du radiateur - chauffe
Donc l'interrupteur télécommandé doit passer sur OFF pour que le radiateur chauffe !

Code : Tout sélectionner

-- script_time_cmdchauffv2.lua ---------------------------------------
-- G PETREMENT -------------------------------------------------------
----------------------------------------------------------------------
-- Modif Chrominator pour adaptation fil pilote radiateur électrique
----------------------------------------------------------------------
-- 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 est de 5min -
----------------------------------------------------------------------

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 = 300                     --Durée de chauffe minimum en secondes (fonction de la chaudière)
-- local InterRelais = 'Piface RelayChaud'   --Sortie vers relais chaudière
local InterRelais = 'Radiateur Salon'     --Sortie vers fil pilote radiateur
local domoticz_url = '127.0.0.1:8080'   --Adresse IP:port de Domoticz
local entree = 'Porte Entrée'  -- Nom de la porte qui stoppera le chauffage en cas d'ouverture
local ssol = 'Porte s/sol'  -- Nom de la porte qui stoppera le chauffage en cas d'ouverture


-- Fonction retournant la différence 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]
local pentree = otherdevices[entree]
local pssol = otherdevices[ssol]

--  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)
   print('-- Porte Entree = '..pentree..' / Porte s/sol ='..pssol)
   
   if  (OnOff == 'On' and pentree == 'Off' and pssol == 'Off') then  --Si l'inter général est ON et les portes fermées
      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]='On'
      print('Cycle trop court :'..Cycle..'s >> Pilote On = Radiateur Off')
      
    elseif Cycle > 870 then --Si le cycle est trop long (> 14min30) on allume sans coupure
      commandArray[InterRelais]='Off'
      print('Cycle long :'..Cycle..'s >> Pilote Off = Radiateur On')
   
    else --Enfin on lance pour X seconde
         print('Cycle lancé pour : '..Cycle..' secondes')
         commandArray[1]={[InterRelais]='Off'}
      commandArray[2]={[InterRelais]='Off AFTER '..tostring(Cycle)}
      end
   else
      commandArray[InterRelais]='On'
      print('Radiateur Off')   
   end
end

return commandArray

Ces scripts fonctionnent selon ce principe depuis Septembre 2015 avec comme résultat une température agréable dans la fameuse pièce difficile à chauffer.
La liaison avec la consigne QIVIVO date de fin Octobre 2015, auparavant la consigne était définie par un Setpoint virtuel de Domoticz.
Dans ma configuration, un planning sur l'interrupteur général active le chauffage d'appoint en fin d'après midi seulement.
Un bon frein vaut mieux que deux sparadraps.

Ubuntu 24.04.3 LTS - Domoticz 2025.1 (build 16782)
Pentium G3220T 2.6GHz - 4Go DDR3
RFXtrx433 USB Ver Pro1/1043
Z-Stick GEN5 Version: 1.6-1136-g07ea22bb
Rtl433 RTL-SDR receiver
RFLink Gateway
Zigbee SLZB-06
samsam51
Messages : 2
Inscription : 23 janv. 2016, 19:39

Re: Fil pilote et régulation PID

Message par samsam51 »

Merci d'avoir pris le temps de faire cette présentation du script :D :D
deennoo
Messages : 4036
Inscription : 25 janv. 2015, 02:00

Re: Fil pilote et régulation PID

Message par deennoo »

Super Boulot !
De quoi se faire Plaisir et essayer d'aider...
http://www.domo-attitude.fr
Répondre