Page 13 sur 34

Re: chauffage PID

Publié : 31 mars 2018, 11:39
par vil1driver
@rimram31,
pour le risque de commandes non transmises, bien joué ;) j'aime beaucoup ton approche avec switch virtuel, et ça évite de casser le Off AFTER.

pour Kp et la température extérieur,
si j'ai tout bien lut les docs,
tout dépends de la méthode choisie, ici nous somme en boucle fermée, donc aucune prise en compte des éléments extérieurs..
certaines régulations ne prennent en compte que la température extérieure, même plus la température intérieure.. régul dite en boucle ouverte
on peut également mixer les 2.. prendre en compte les ouvertures de fenêtre, la mise en route de la vmc ou du four..etc..
des choses qui peuvent être sacrément complexes à modéliser,
n'étant pas du tout du métier, j'ai déjà eu bien du mal à obtenir un truc qui me plaise :) (qui tienne la route)
maintenant si on peut aller plus loin, pourquoi pas :D

Kp n'est finalement pas le plus important, de mon expérience le facteur le plus actif est Ki.
toujours est-il qu'ici lors changement de consigne (17 -> 19) le chauffage va passer de 11% à 100% directement et commencer à abaisser le temps de chauffe que vers 18.5 voir 18.8, s'il fait très froid dehors la courbe va l'indiquer, la température va effectivement monter plus lentement et la régule va en tenir compte (Kd) et maintenir un effort de chauffe plus important.
une fois stabilisée, l'intégrale aura simplement une valeur plus importante s'il fait très froid dehors..

@Hawk49,
pour l'anticipation il faudrait tenter des stats pour voir si l'on peut trouver une corrélation entre temps de montée en température et delta temp int/ext

Re: chauffage PID

Publié : 01 avr. 2018, 07:38
par Hawk49
Désolé les gars, je lance la conversation mais sans avoir eu le temps de me pencher dessus depuis... (en même temps, je soupçonne que vous aimez vous creuser la tête mais je voudrais pas passer pour celui qui va sur un forum pour pas chercher lui-même).

@vil1driver,
Pour la montée en température puis sa stabilisation, je suis d'accord avec toi sur l'impact des coef et le fait que Kd corrigera le fait que ça chauffe plus ou moins vite. Au pire, les coefficients ne seront pas optimaux avec les conditions mais je ne suis sûr pas ce soit décelable pour une régul de température (on n'est pas en train de piloter une voiture autonome non plus !).
En revanche tu as bien compris mon enjeu: savoir combien de temps à l'avance lancer le chauffage pour obtenir la bonne température à l'heure voulue. Et c'est pour cela que j'aimerais que le système évalue (automatiquement dans l'idéal) un autre coefficient qui correspondrait à l'isolation de la pièce en quelque sorte. Et c'est fait dans l'algo SVT ("Each room (or area) has their own characteristics: thermal inertia – heating power – outside Insulation.") Parmi ses paramètres, il utilise ConstC ("coef de chauffage interne") qui dépend de la taille de la pièce et de la capacité du radiateur et ConstT ("coef de chauffage externe") qui dépend de l'isolation avec l'extérieur. Avec ces valeurs, on pourrait et affiner Kp, Ki et Kd et calculer l'anticipation que je recherche.
Sinon, OK aussi avec toi sur le fait qu'on puisse faire des stats sur l'efficacité du chauffage en fonction du delta avec l'extérieur mais au final, cela ne revient-il pas à faire le même boulot ?

Re: chauffage PID

Publié : 01 avr. 2018, 09:01
par vil1driver
pour moi anticipation et régulation sont à dissocier.

le jeux est juste de savoir quand réclamer le changement de consigne.

la montée en température est à peut près linéaire et (dans une pièce ici) est d'environ 1°C/h (4.5°C dehors)
je refais le même constat avec une température ext de 12°C par exemple et j'aurais une idée de l'impact..
Réaliser sa propre 'courbe d'eau'
c'est grossier (non prise en compte du vent, de l'ensoleillement, etc..) mais doit convenir

Re: chauffage PID

Publié : 01 avr. 2018, 10:10
par rimram31
[quote="vil1driver"]...la montée en température est à peut près linéaire .../quote]Je viens de comprendre (suis lent parfois :-)) tu as raison, le Kp n'a pas beaucoup d'importance et quand je regarde mes courbes de T, seules la ou les pièces ou le chauffage est limite (en capacité/volume) sont influencées par la T extérieure et quoiqu'il en soit, la chauffe sera a 100% les 3/4 du temps.

Donc effectivement, la qualité de la régulation va se jouer dans l'approche de la T de consigne et dans son maintien ensuite. Perso chez moi (hysteresis simple) j'ai introduit une pause dans la chauffe matinale pour limiter le dépassement lorsque la T de consigne est atteinte.

D'où mon idée qu'il doit y avoir moyen de se servir des observations sur un démarrage hysteresis pour déterminer les coeff du PID, il y a des méthodes empiriques qui peuvent peut-être le permettre. Bon reste plus qu'a trouver quand/comment après réajuster les coeffs :D

@Hawk49 ne t'inquiète pas, le sujet m'intéresse, rien de plus, j'ai toujours une régulation des plus basique chez moi. Pour ton souci d'avoir la T voulue au bon moment, le paramètre principal va être le créneau de programmation du thermostat. Ce qu'apporte un PID, c'est de ne pas avoir de rebond et/ou d'oscillations lorsque la T est atteinte.

Re: chauffage PID

Publié : 01 avr. 2018, 16:40
par vil1driver

Re: chauffage PID

Publié : 02 avr. 2018, 11:07
par rimram31
Post très intéressant, le contrôle thermique est un sujet passionnant (comme je le disais plus haut, mon premier métier même si j'ai appris a réguler en T des satellites plus que des batiments :-)).

Il n'est pas obligatoirement intéressant effectivement d'abaisser (ou d'abaisser de trop) la T dite "eco", tout comme il n'est pas obligatoirement rentable de ne pas chauffer des zones de son habitation non habitées (chambre d'ami, bureau ...), de se mettre en hors gel un we d'absence ... d'autant moins que la performance thermique (isolation) est bonne. Mais le sujet est complexe et la réponse dépend de nombreux paramètres dont les conditions extérieures sur lesquelles nous n'avons pas trop la main.

L'intérêt de s'équiper d'une solution comme les notres c'est de pouvoir observer et ajuster alors selon. J'avais un système auparavant "auto" mais sans mesure, j'ai depuis mon équipement pu ajuster pus finement en fonction des T observées vs celle que je visais.

Ca peut permettre de répondre a ton souci @Hawk49. En observant, tu seras capable de déterminer les heures de déclenchement (confort) pour être assuré que la T sera obtenue a l'heure voulue. Le paramètre inconnu est la T réelle qu'il fera en T eco, car selon qu'il fait +/- froid, la T eco n'est pas nécessairement atteinte entre deux périodes confort. Perso j'ai accepté qu'il puisse faire 1/2 ou 1° de moins a mon réveil selon la T ext, je me dis que c'est pas plus mal au final (pas fan du confort "absolu").

Re: chauffage PID

Publié : 03 avr. 2018, 11:14
par Hawk49
Alors alors,

Voici l'algo de Logread en python dispo ici https://github.com/999LV/SmartVirtualThermostat:

Code : Tout sélectionner

"""
<plugin key="SVT" name="Smart Virtual Thermostat" author="logread" version="0.3.8" wikilink="https://www.domoticz.com/wiki/Plugins/Smart_Virtual_Thermostat.html" externallink="https://github.com/999LV/SmartVirtualThermostat.git">
    <params>
        <param field="Address" label="Domoticz IP Address" width="200px" required="true" default="127.0.0.1"/>
        <param field="Port" label="Port" width="40px" required="true" default="8080"/>
        <param field="Username" label="Username" width="200px" required="false" default=""/>
        <param field="Password" label="Password" width="200px" required="false" default=""/>
        <param field="Mode1" label="Inside Temperature Sensors (csv list of idx)" width="100px" required="true" default="0"/>
        <param field="Mode2" label="Outside Temperature Sensors (csv list of idx)" width="100px" required="false" default=""/>
        <param field="Mode3" label="Heating Switches (csv list of idx)" width="100px" required="true" default="0"/>
        <param field="Mode5" label="Calculation cycle, Minimum Heating time per cycle, Pause On delay, Pause Off delay, Forced mode duration (all in minutes)" width="200px" required="true" default="30,0,2,1,60"/>
        <param field="Mode6" label="Logging Level" width="75px">
            <options>
                <option label="Debug" value="Debug"/>
                <option label="Verbose" value="Verbose"/>
                <option label="Normal" value="Normal"  default="true" />
            </options>
        </param>
    </params>
</plugin>

"""

import Domoticz
import json
import urllib.parse as parse
import urllib.request as request
from datetime import datetime, timedelta
import time
import base64

class deviceparam:

    def __init__(self, unit, nvalue, svalue):
        self.unit = unit
        self.nvalue = nvalue
        self.svalue = svalue


class BasePlugin:

    def __init__(self):
        self.debug = False
        self.calculate_period = 30  # Time in minutes between two calculations (cycle)
        self.minheatpower = 0  # if heating is needed, minimum heat power (in % of calculation period)
        self.deltamax = 0.2  # allowed temp excess over setpoint temperature
        self.pauseondelay = 2  # time between pause sensor actuation and actual pause
        self.pauseoffdelay = 1  # time between end of pause sensor actuation and end of actual pause
        self.forcedduration = 60  # time in minutes for the forced mode
        self.InTempSensors = []
        self.OutTempSensors = []
        self.Heaters = []
        self.InternalsDefaults = {
            'ConstC': 60,  # inside heating coeff, depends on room size & power of your heater (60 by default)
            'ConstT': 1,  # external heating coeff,depends on the insulation relative to the outside (1 by default)
            'nbCC': 0,  # number of learnings for ConstC
            'nbCT': 0,  # number of learnings for ConstT
            'LastPwr': 0,  # % power from last calculation
            'LastInT': 0,  # inside temperature at last calculation
            'LastOutT': 0,  # outside temperature at last calculation
            'LastSetPoint': 20,  # setpoint at time of last calculation
            'ALStatus': 0}  # AutoLearning status (0 = uninitialized, 1 = initialized, 2 = disabled)
        self.Internals = self.InternalsDefaults.copy()
        self.heat = False
        self.pause = False
        self.pauserequested = False
        self.pauserequestchangedtime = datetime.now()
        self.forced = False
        self.intemp = 20.0
        self.outtemp = 20.0
        self.setpoint = 20.0
        self.endheat = datetime.now()
        self.nextcalc = self.endheat
        self.lastcalc = self.endheat
        self.nextupdate = self.endheat
        self.nexttemps = self.endheat
        self.learn = True
        return


    def onStart(self):
        if Parameters["Mode6"] == 'Debug':
            self.debug = True
            Domoticz.Debugging(1)
            DumpConfigToLog()
        else:
            Domoticz.Debugging(0)

        # create the child devices if these do not exist yet
        devicecreated = []
        if 1 not in Devices:
            Options = {"LevelActions": "||",
                       "LevelNames": "Off|Auto|Forced",
                       "LevelOffHidden": "false",
                       "SelectorStyle": "0"}
            Domoticz.Device(Name="Thermostat Control", Unit=1, TypeName="Selector Switch", Switchtype=18, Image=15,
                            Options=Options, Used=1).Create()
            #Devices[1].Update(nValue=0, sValue="0")  # default is Off state
            devicecreated.append(deviceparam(1, 0, "0"))  # default is Off state
        if 2 not in Devices:
            Options = {"LevelActions": "||",
                       "LevelNames": "Off|Normal|Economy",
                       "LevelOffHidden": "true",
                       "SelectorStyle": "0"}
            Domoticz.Device(Name="Thermostat Mode", Unit=2, TypeName="Selector Switch", Switchtype=18, Image=15,
                            Options=Options, Used=1).Create()
            devicecreated.append(deviceparam(2, 0, "10"))  # default is normal mode
        if 3 not in Devices:
            Domoticz.Device(Name="Thermostat Pause", Unit=3, TypeName="Switch", Image=9, Used=1).Create()
            devicecreated.append(deviceparam(3, 0, ""))  # default is Off
        if 4 not in Devices:
            Domoticz.Device(Name="Setpoint Normal", Unit=4, Type=242, Subtype=1, Used=1).Create()
            devicecreated.append(deviceparam(4, 0, "20"))  # default is 20 degrees
        if 5 not in Devices:
            Domoticz.Device(Name="Setpoint Economy", Unit=5, Type=242, Subtype=1, Used=1).Create()
            devicecreated.append(deviceparam(5 ,0, "20"))  # default is 20 degrees
        if 6 not in Devices:
            Domoticz.Device(Name="Thermostat temp", Unit=6, TypeName="Temperature").Create()
            devicecreated.append(deviceparam(6, 0, "20"))  # default is 20 degrees

        # if any device has been created in onStart(), now is time to update its defaults
        for device in devicecreated:
            Devices[device.unit].Update(nValue=device.nvalue, sValue=device.svalue)

        # build lists of sensors and switches
        self.InTempSensors = parseCSV(Parameters["Mode1"])
        Domoticz.Debug("Inside Temperature sensors = {}".format(self.InTempSensors))
        self.OutTempSensors = parseCSV(Parameters["Mode2"])
        Domoticz.Debug("Outside Temperature sensors = {}".format(self.OutTempSensors))
        self.Heaters = parseCSV(Parameters["Mode3"])
        Domoticz.Debug("Heaters = {}".format(self.Heaters))

        # splits additional parameters
        params = parseCSV(Parameters["Mode5"])
        if len(params) == 5:
            self.calculate_period = params[0]
            if self.calculate_period < 5:
                Domoticz.Error("Invalid calculation period parameter. Using minimum of 5 minutes !")
                self.calculate_period = 5
            self.minheatpower = params[1]
            if self.minheatpower > 100:
                Domoticz.Error("Invalid minimum heating parameter. Using maximum of 100% !")
                self.minheatpower = 100
            self.pauseondelay = params[2]
            self.pauseoffdelay = params[3]
            self.forcedduration = params[4]
            if self.forcedduration < 30:
                Domoticz.Error("Invalid forced mode duration parameter. Using minimum of 30 minutes !")
                self.calculate_period = 30
        else:
            Domoticz.Error("Error reading Mode5 parameters")

        # loads persistent variables from dedicated user variable
        # note: to reset the thermostat to default values (i.e. ignore all past learning),
        # just delete the relevant "<plugin name>-InternalVariables" user variable Domoticz GUI and restart plugin
        self.getUserVar()

        # if mode = off then make sure actual heating is off just in case if was manually set to on
        if Devices[1].sValue == "0":
            self.switchHeat(False)


    def onStop(self):
        Domoticz.Debugging(0)


    def onCommand(self, Unit, Command, Level, Hue):
        Domoticz.Debug("onCommand called for Unit {}: Command '{}', Level: {}".format(Unit, Command, Level))
        if Unit == 3:
            self.pauserequestchangedtime = datetime.now()
            svalue = ""
            if str(Command) == "On":
                nvalue = 1
                self.pauserequested = True
            else:
                nvalue = 0
                self.pauserequested = False
        else:
            if Level > 0:
                nvalue = 1
            else:
                nvalue = 0
            svalue = str(Level)
        Devices[Unit].Update(nValue=nvalue, sValue=svalue)
        if Unit in (1, 2, 4, 5): # force recalculation if control or mode or a setpoint changed
            self.nextcalc = datetime.now()
            self.learn = False
            self.onHeartbeat()


    def onHeartbeat(self):

        now = datetime.now()

        if Devices[1].sValue == "0":  # Thermostat is off
            if self.forced or self.heat:  # thermostat setting was just changed so we kill the heating
                self.forced = False
                self.endheat = now
                Domoticz.Debug("Switching heat Off !")
                self.switchHeat(False)

            if self.nexttemps <= now:
                # call the Domoticz json API for a temperature devices update, to get the lastest temps...
                self.readTemps()

        elif Devices[1].sValue == "20":  # Thermostat is in forced mode
            if self.forced:
                if self.endheat <= now:
                    self.forced = False
                    self.endheat = now
                    Domoticz.Debug("Forced mode Off !")
                    Devices[1].Update(nValue=1, sValue="10")  # set thermostat to normal mode
                    self.switchHeat(False)
            else:
                self.forced = True
                self.endheat = now + timedelta(minutes=self.forcedduration)
                Domoticz.Debug("Forced mode On !")
                self.switchHeat(True)

            if self.nexttemps <= now:
                # call the Domoticz json API for a temperature devices update, to get the lastest temps...
                self.readTemps()

        else:  # Thermostat is in mode auto

            if self.forced:  # thermostat setting was just changed from "forced" so we kill the forced mode
                self.forced = False
                self.endheat = now
                self.nextcalc = now   # this will force a recalculation on next heartbeat
                Domoticz.Debug("Forced mode Off !")
                self.switchHeat(False)

            elif (self.endheat <= now or self.pause) and self.heat:  # heat cycle is over
                self.endheat = now
                self.heat = False
                if self.Internals['LastPwr'] < 100:
                    self.switchHeat(False)
                # if power was 100(i.e. a full cycle), then we let the next calculation (at next heartbeat) decide
                # to switch off in order to avoid potentially damaging quick off/on cycles to the heater(s)

            elif self.pause and not self.pauserequested:  # we are in pause and the pause switch is now off
                if self.pauserequestchangedtime + timedelta(minutes=self.pauseoffdelay) <= now:
                    Domoticz.Debug("Pause is now Off")
                    self.pause = False

            elif not self.pause and self.pauserequested:  # we are not in pause and the pause switch is now on
                if self.pauserequestchangedtime + timedelta(minutes=self.pauseondelay) <= now:
                    Domoticz.Debug("Pause is now On")
                    self.pause = True
                    self.switchHeat(False)

            elif self.pause and self.nexttemps <= now:  # added to update thermostat temp even in pause mode
                # call the Domoticz json API for a temperature devices update, to get the lastest temps...
                self.readTemps()

            elif (self.nextcalc <= now) and not self.pause:  # we start a new calculation
                self.nextcalc = now + timedelta(minutes=self.calculate_period)
                Domoticz.Debug("Next calculation time will be : " + str(self.nextcalc))

                # make current setpoint used in calculation reflect the select mode (10= normal, 20 = economy)
                if Devices[2].sValue == "10":
                    self.setpoint = float(Devices[4].sValue)
                else:
                    self.setpoint = float(Devices[5].sValue)

                # call the Domoticz json API for a temperature devices update, to get the lastest temps...
                if self.readTemps():
                    # do the thermostat work
                    self.AutoMode()
                else:
                    # make sure we switch off heating if there was an error with reading the temp
                    self.switchHeat(False)

        # check if need to refresh setpoints so that they do not turn red in GUI
        if self.nextupdate <= now:
            self.nextupdate = now + timedelta(minutes=int(Settings["SensorTimeout"]))
            Devices[4].Update(nValue=0, sValue=Devices[4].sValue)
            Devices[5].Update(nValue=0, sValue=Devices[5].sValue)


    def AutoMode(self):
        if self.intemp > self.setpoint + self.deltamax:
            Domoticz.Debug("Temperature exceeds setpoint: no heating")
            power = 0
        else:
            if self.learn:
                self.AutoCallib()
            else:
                self.learn = True
            if self.outtemp is None:
                power = round((self.setpoint - self.intemp) * self.Internals["ConstC"], 1)
            else:
                power = round((self.setpoint - self.intemp) * self.Internals["ConstC"] +
                              (self.setpoint - self.outtemp) * self.Internals["ConstT"], 1)

        if power < 0:
            power = 0  # lower limit
        elif power > 100:
            power = 100  # upper limit
        if power <= self.minheatpower:
            power = self.minheatpower  # minimum heating per cycle in % of cycle time

        heatduration = round(power * self.calculate_period / 100)
        WriteLog("Calculation: Power = {} -> heat duration = {} minutes".format(power, heatduration), "Verbose")

        if power == 0:
            self.switchHeat(False)
            Domoticz.Debug("No heating required !")
        else:
            self.endheat = datetime.now() + timedelta(minutes=heatduration)
            Domoticz.Debug("End Heat time = " + str(self.endheat))
            self.switchHeat(True)
            if self.Internals["ALStatus"] < 2:
                self.Internals['LastPwr'] = power
                self.Internals['LastInT'] = self.intemp
                self.Internals['LastOutT'] = self.outtemp
                self.Internals['LastSetPoint'] = self.setpoint
                self.Internals['ALStatus'] = 1
                self.saveUserVar()  # update user variables with latest learning

        self.lastcalc = datetime.now()


    def AutoCallib(self):
        now = datetime.now()
        if self.Internals['ALStatus'] != 1:  # not initalized... do nothing
            Domoticz.Debug("Fist pass at AutoCallib... no callibration")
            pass
        elif self.Internals['LastPwr'] == 0:  # heater was off last time, do nothing
            Domoticz.Debug("Last power was zero... no callibration")
            pass
        elif self.Internals['LastPwr'] == 100 and self.intemp < self.Internals['LastSetPoint']:
            # heater was on max but setpoint was not reached... no learning
            Domoticz.Debug("Last power was 100% but setpoint not reached... no callibration")
            pass
        elif self.intemp > self.Internals['LastInT'] and self.Internals['LastSetPoint'] > self.Internals['LastInT']:
            # learning ConstC
            ConstC = (self.Internals['ConstC'] * ((self.Internals['LastSetPoint'] - self.Internals['LastInT']) /
                                                  (self.intemp - self.Internals['LastInT']) *
                                                  (timedelta.total_seconds(now - self.lastcalc) /
                                                   (self.calculate_period * 60))))
            WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose")
            self.Internals['ConstC'] = round((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) /
                                             (self.Internals['nbCC'] + 1), 1)
            self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, 50)
            WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose")
        elif self.outtemp is not None and self.Internals['LastSetPoint'] > self.Internals['LastOutT']:
            # learning ConstT
            ConstT = (self.Internals['ConstT'] + ((self.Internals['LastSetPoint'] - self.intemp) /
                                                  (self.Internals['LastSetPoint'] - self.Internals['LastOutT']) *
                                                  self.Internals['ConstC'] *
                                                  (timedelta.total_seconds(now - self.lastcalc) /
                                                   (self.calculate_period * 60))))
            WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose")
            self.Internals['ConstT'] = round((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) /
                                             (self.Internals['nbCT'] + 1), 1)
            self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, 50)
            WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose")


    def switchHeat(self, switch):
        if switch:  # heating on
            self.heat = True
            Domoticz.Debug("Heating On")
            # switch on heater(s)
            for heater in self.Heaters:
                DomoticzAPI("type=command&param=switchlight&idx={}&switchcmd=On".format(heater))
            Domoticz.Debug("End Heat time = " + str(self.endheat))
        else:
            self.heat = False
            Domoticz.Debug("Heating Off")
            # switch off heater(s)
            for heater in self.Heaters:
                DomoticzAPI("type=command&param=switchlight&idx={}&switchcmd=Off".format(heater))


    def readTemps(self):
        # set update flag for next temp update (used only when in off, forced mode or pause is active)
        self.nexttemps = datetime.now() + timedelta(minutes=self.calculate_period)

        # fetch all the devices from the API and scan for sensors
        noerror = True
        listintemps = []
        listouttemps = []
        devicesAPI = DomoticzAPI("type=devices&filter=temp&used=true&order=Name")
        if devicesAPI:
            for device in devicesAPI["result"]:  # parse the devices for temperature sensors
                idx = int(device["idx"])
                if idx in self.InTempSensors:
                    if "Temp" in device:
                        Domoticz.Debug("device: {}-{} = {}".format(device["idx"], device["Name"], device["Temp"]))
                        # check temp sensor is not timed out
                        if not SensorTimedOut(device["LastUpdate"]):
                            listintemps.append(device["Temp"])
                        else:
                            Domoticz.Error("skipping timed out temperature sensor {}".format(device["Name"]))
                    else:
                        Domoticz.Error("device: {}-{} is not a Temperature sensor".format(device["idx"], device["Name"]))
                elif idx in self.OutTempSensors:
                    if "Temp" in device:
                        Domoticz.Debug("device: {}-{} = {}".format(device["idx"], device["Name"], device["Temp"]))
                        # check temp sensor is not timed out
                        if not SensorTimedOut(device["LastUpdate"]):
                            listouttemps.append(device["Temp"])
                        else:
                            Domoticz.Error("skipping timed out temperature sensor {}".format(device["Name"]))
                    else:
                        Domoticz.Error("device: {}-{} is not a Temperature sensor".format(device["idx"], device["Name"]))

        # calculate the average inside temperature
        nbtemps = len(listintemps)
        if nbtemps > 0:
            self.intemp = round(sum(listintemps) / nbtemps, 1)
            Devices[6].Update(nValue=0,
                              sValue=str(self.intemp))  # update the dummy device showing the current thermostat temp
        else:
            Domoticz.Error("No Inside Temperature found... Switching Thermostat Off")
            Devices[1].Update(nValue=0, sValue="0")  # switch off the thermostat
            noerror = False

        # calculate the average outside temperature
        nbtemps = len(listouttemps)
        if nbtemps > 0:
            self.outtemp = round(sum(listouttemps) / nbtemps, 1)
        else:
            Domoticz.Debug("No Outside Temperature found...")
            self.outtemp = None

        WriteLog("Inside Temperature = {}".format(self.intemp), "Verbose")
        WriteLog("Outside Temperature = {}".format(self.outtemp), "Verbose")
        return noerror


    def getUserVar(self):
        variables = DomoticzAPI("type=command&param=getuservariables")
        if variables:
            # there is a valid response from the API but we do not know if our variable exists yet
            novar = True
            varname = Parameters["Name"] + "-InternalVariables"
            valuestring = ""
            if "result" in variables:
                for variable in variables["result"]:
                    if variable["Name"] == varname:
                        valuestring = variable["Value"]
                        novar = False
                        break
            if novar:
                # create user variable since it does not exist
                WriteLog("User Variable {} does not exist. Creation requested".format(varname), "Verbose")
                DomoticzAPI("type=command&param=saveuservariable&vname={}&vtype=2&vvalue={}".format(
                    varname, str(self.InternalsDefaults)))
                self.Internals = self.InternalsDefaults.copy()  # we re-initialize the internal variables
            else:
                try:
                    self.Internals.update(eval(valuestring))
                except:
                    self.Internals = self.InternalsDefaults.copy()
                return
        else:
            Domoticz.Error("Cannot read the uservariable holding the persistent variables")
            self.Internals = self.InternalsDefaults.copy()


    def saveUserVar(self):
        varname = Parameters["Name"] + "-InternalVariables"
        DomoticzAPI("type=command&param=updateuservariable&vname={}&vtype=2&vvalue={}".format(
            varname, str(self.Internals)))


global _plugin
_plugin = BasePlugin()


def onStart():
    global _plugin
    _plugin.onStart()


def onStop():
    global _plugin
    _plugin.onStop()


def onCommand(Unit, Command, Level, Hue):
    global _plugin
    _plugin.onCommand(Unit, Command, Level, Hue)


def onHeartbeat():
    global _plugin
    _plugin.onHeartbeat()


# Plugin utility functions ---------------------------------------------------

def SensorTimedOut(datestring):
    def LastUpdate(datestring):
        dateformat = "%Y-%m-%d %H:%M:%S"
        # the below try/except is meant to address an intermittent python bug in some embedded systems
        try:
            result = datetime.strptime(datestring, dateformat)
        except TypeError:
            result = datetime(*(time.strptime(datestring, dateformat)[0:6]))
        return result
    return LastUpdate(datestring) + timedelta(minutes=int(Settings["SensorTimeout"])) < datetime.now()

def parseCSV(strCSV):
    listvals = []
    for value in strCSV.split(","):
        try:
            val = int(value)
        except:
            pass
        else:
            listvals.append(val)
    return listvals


def WriteLog(message, level="Normal"):
    if Parameters["Mode6"] == "Verbose" or Parameters["Mode6"] == "Debug":
        Domoticz.Log(message)
    elif level == "Normal":
        Domoticz.Log(message)


def DomoticzAPI(APICall):
    resultJson = None
    url = "http://{}:{}/json.htm?{}".format(Parameters["Address"], Parameters["Port"], parse.quote(APICall, safe="&="))
    try:
        req = request.Request(url)
        if Parameters["Username"] != "":
            WriteLog("Add authentification for user {}".format(Parameters["Username"]), "Debug")
            credentials = ('%s:%s' % (Parameters["Username"], Parameters["Password"]))
            encoded_credentials = base64.b64encode(credentials.encode('ascii'))
            req.add_header('Authorization', 'Basic %s' % encoded_credentials.decode("ascii"))

        response = request.urlopen(req)
        if response.status == 200:
            resultJson = json.loads(response.read().decode('utf-8'))
            if resultJson["status"] != "OK":
                Domoticz.Error("Domoticz API returned an error: status = {}".format(resultJson["status"]))
                resultJson = None
        else:
            Domoticz.Error("Domoticz API: http error = {}".format(response.status))
    except:
        Domoticz.Error("Error calling '{}'".format(url))
    return resultJson


# Generic helper functions
def DumpConfigToLog():
    for x in Parameters:
        if Parameters[x] != "":
            Domoticz.Debug("'" + x + "':'" + str(Parameters[x]) + "'")
    Domoticz.Debug("Device count: " + str(len(Devices)))
    for x in Devices:
        Domoticz.Debug("Device:           " + str(x) + " - " + str(Devices[x]))
        Domoticz.Debug("Device ID:       '" + str(Devices[x].ID) + "'")
        Domoticz.Debug("Device Name:     '" + Devices[x].Name + "'")
        Domoticz.Debug("Device nValue:    " + str(Devices[x].nValue))
        Domoticz.Debug("Device sValue:   '" + Devices[x].sValue + "'")
        Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
    return

    © 2018 GitHub, Inc.
    Terms
    Privacy
    Security
    Status
    Help

    Contact GitHub
    API
    Training
    Shop
    Blog
    About

Press h to open a hovercard with more details.
Voici l'analyse que j'en fait: (je m'excuse d'avance pour les approximations qui j’espère ne seront pas trop hasardeuses dans mon pseudo-code)

Il s'avère que c'est un "double" algo P (et non PID) qui ajuste le coefficient Kp (appelé ici ConstP) automatiquement et qui en rajoute un, disons Ke (appellé ici ConstT) par rapport à l'écart (consigne - temperature extérieure).

Le calcul du temps de chauffe se fait ainsi:

Code : Tout sélectionner

FONCTION AutoMode()
//un peu d'hysterisis:
SI (temperature dépasse la consigne d'un seuil prédéfini)
  tempsChauffe = 0
SINON //calcul du temps de chauffe

	SI (quelque chose a changé (consigne, mode, ...))
	  on ne fait pas d'autocalibrage ce coup ci (repris au prochain calcul)
	FINSI   //pas clair pour moi car depend du parametre Unit dans la fonction onCommand et je ne comprends pas bien à quoi cela correspond...
 
	SI (pas de temperature extérieure disponible)  //mode P simple
	  power = ConstC * err /100
	SINON
	  power = [ ConstC * err + ConstT * (temperatureConsigne - temperatureExterieure) ] /100 
	FINSI
	
	//filtrage des valeurs trop petites ou trop grandes
	power = constrain(power, min, 100%)  //min peut etre 0 ou plus si on ne veut pas éteindre completement un plancher chauffant par ex.

	tempsChauffe = power * PeriodeDeCalcul
FINSI

return tempsChauffe 

FINAutoMode


//calibrage auto: on part de ConstC = 60 et ConstT = 1
// ConstC va baisser petit à petit et ConstT augmenter
FONCTION autoCalib()

SI (si on a chauffé et qu'on a dépassé la température de consigne)	//affinage de ConstC

  aux = ConstC * (temperature qu'il fallait gagner / temperature gagnée) * %deChauffe
  //mise à jour de ConstC  en faisant la moyenne des nbCC anciennes valeurs et de cette nouvelle (limité, en gros, aux 50 derniers apprentissages puisque nbCC <= 50)
  ConstC = (ConstC * nbCC + aux)/(nbCC+1)
 
SINON  SI (on a chauffé sans atteindre la consigne et consigne > temperature exterieure)	//affinage de ConstT
  
  aux = ConstT * [1 + (temperature qu'il fallait gagner) / (tempConsigne - temperatureExterieure) * %deChauffe ]
  
  //mise à jour de ConstT  en faisant la moyenne des nbCT anciennes valeurs et de cette nouvelle (limité, en gros, aux 50 derniers apprentissages puisque nbCT <= 50)
  ConstT = (ConstT * nbCT + aux)/(nbCT+1)
  
FinSINON

FINautoCalib
  
Au final, avant d'essayer de calculer finenenent Tp, Tc, θp comme Greg pour obtenir des valeurs théoriques de Kc, Ki et Kd, je pense garder l'algo PID de Vil1driver, en l'initialisant avec:
Ki = Kd = 0
Kp et Ke en autocalibrage avec le principe ci-dessus pendant 2 à 3 jours (respectivement ConstP et ConstT)
puis ajuster Ki et Kd manuellement (tout en laissant l'autocalibrage ?)

@vil1driver: merci, j'ai lu aussi entre temps le principe de la courbe d'eau (http://blog.elyotherm.fr/2013/08/reglag ... auffe.html) et ce sera certainement une inspiration pour anticiper le chauffage (avec le calcule de Ke ?). Bref, beaucoup de tests en perspective !
Sachant qu'au final, la seule solution théoriquement "propre" serait de déterminer une fois pour toute Kp, Tc, θp et utiliser les équations différentielles pour modéliser la pièce et anticiper la température future en fonction de la température extérieure et de la consigne qu'on envisage d'appliquer (du prédictif quoi).

[edit]
bon, je bosse sur une implémentation de tout ça mais je rame, je rame... Enfin, je devrais dire "j'apprends" ! J'ai forcément dû raconter des bêtises parmi ce qui est écrit au dessus...
Je ferai un retour quand j'aurai obtenu quelque chose de satisfaisant.

Re: chauffage PID

Publié : 08 avr. 2018, 21:01
par Hawk49
Voilà des nouvelles du front:
j'ai eu la chance de remarquer que cette nuit la température extérieure a été très stable. Et en plus, le chauffage du salon était éteint.
Donc, l'évolution de la température du salon n'a dépendu que de l'écart avec l'extérieur. Voici un peu de math...
les données:
entre 23h et 6h:
- La température extérieure (=température ressentie pour une fois) est passé de 11.6°C à 10.5°C, je l'arrondie à une température constante de 11°C.
- La température intérieure est passé de 18.7°C à 17.7°C

La dérivée de la température intérieure est proportionnelle à l'écart avec l'extérieur, avec f la fonction qui associe la température intérieure à t (le temps en heures) et Tx la température extérieure:
f '(t) = a ( f(t) - Tx )
d'où f de la forme: f(t) = l e^(at) + Tx (solutions type de l'équation différentielle précédente)
en prenant f(0) = 18.7, on trouve l = 7.7 (e^0 = 1 pour les souvenirs du lycée...)
donc f(t) = 7.7 e^(at) + 11
avec maintenant f(7) = 17.7, on trouve a = ( ln (17.7-Tx) - ln a ) / 7 = ( ln (6.7) - ln 7.7 ) / 7 = -0.01987...

En arrondissant a = -0.02 = -2%, j'en conclue que ma pièce perd 0.02 °C par heure et par °C d'écart avec l'extérieur.
La fonction f(t) = 7.7 * e^(-0.02 t) +11 accompagne bien l'historique des températures de cette nuit et semble être une bonne modélisation.
Donc, ce que je compte faire, c'est laisser tomber l'histoire du Ke et viser systématiquement une consigne légèrement surévaluée pour tenir compte de la température extérieure.
Par exemple:
je souhaite 19°C et il fait 17°C dans ma pièce, il fait -5°C dehors. Sans chauffage, j'estime que ma pièce perd à ce moment 0.44°C par heure. Avec mes cycle de 10 minutes dans l'algorithme du PID, elle devrait donc perdre 0.07°C sur le prochain cycle. Je calcule donc le temps de chauffe pour une consigne de 19.07°C.

Voilà pour le principe. Le 0.07°C de consigne en plus me semble insignifiant... Peut être faut-il voir sur une heure (ou autre) au lieu de 10 min, je ne sais pas encore. Il faut que je mette ça en place pour test.

Finalement, je compte:
- mettre les coefficients initiaux à: Kp = 100, Ki = Kd =0
- prendre en compte la température extérieure comme je viens de l'expliquer
- faire un apprentissage automatique de Kp avec pour objectif de ne pas dépasser la consigne de plus de 5% (1°C pour une consigne de 20°C donc)
- ajuster ensuite Ki puis Kd

Si ça peut inspirer du monde.. (pour l'instant, avec mon 0.07°C, je penche pour l'avis de Vil1driver: "OSEF ! Le PID va se débrouiller !")

Re: chauffage PID

Publié : 09 avr. 2018, 11:18
par rimram31
Je reste circonspect sur le lien avec la T extérieure ou alors je comprends plus rien suite aux remarques de vil1driver.

Par principe même l'algo de régulation PID et surement un simple P aussi (proportionnel uniquement Kp) va augmenter le temps de chauffe (% de chauffe dans l'intervalle) si l'écart (pour un cycle) est grand par rapport a la consigne souhaitée donc "naturellement" plus il fera froid plus l'écart est important (déperdition) plus il va chauffer (composante proportionnelle) et ce sans besoin de correctif supplémentaire.

Ou alors j'ai raté un truc là ...

Pour ce qui est de la détermination automatique des paramètres de la régulation, il existe des méthodes empiriques apparemment plus ou moins utilisables, par exemple: http://www.specialautom.net/synthese-empirique ou http://www.rhaaa.fr/regulation-pid-comment-la-regler-12. Si on pars en régulation hystérésis en mémorisant des points de mesure, on peut peut-être alors déterminer les paramètres "automatiquement".

Re: chauffage PID

Publié : 09 avr. 2018, 23:28
par Hawk49
Donc tu rajoutes +1 pour le "OSEF le PID va se débrouiller !".

Je me prends peut être trop la tête...
Par rapport à ce que tu dit rimram31, ce serait plutôt le D du PID qui prend en compte la vitesse de changement de température de la pièce et contrecarre les écarts de température avec l'extérieur (qui joue sur la vitesse de montée en température). Pour ce qui est du paramétrage du PID, le choix des 3 coefficients se fait en théorie pour une consigne et des conditions données. En théorie toujours, si les conditions changent (comme la température extérieure) ou la consigne, les coefficients obtenus par les méthodes que tu cites changent aussi (j'avais déjà lu le site de ton 1er lien mais je ne connaissais pas le 2ème, très intéressant).
Le problème avec la température d'une pièce, c'est que les conditions changent continuellement !!! (Ce qui rend difficile la lecture graphique soit dit en passant). Alors je me prends beaucoup la tête mais mon but était de trouver une solution pour éviter l'influence de la température extérieure et ainsi considérer que mes 3 coefficients restent valables... Mais comme je ne suis pas du domaine, je me fourre peut être le doigt dans l’œil sur la méthode.
Ce que je voudrais, c'est:
- un Kp qui assure une montée rapide mais sans trop dépasser la consigne
- un Ki pour gagner en précision et mieux respecter la consigne une fois stabilisé
- un Kd qui évite trop d'oscillations avant la stabilité
Et que chacun reste dans son rôle !
En oubliant le four, les amis éméchés et autres perturbations sympathiques (clin d’œil à vil1driver), je considère que la température de la pièce dépend de l'énergie transmise par le radiateur (caractéristiques du radiateur et de la pièce modélisées par Kp) et de l'énergie perdue vers l'extérieur (ma préoccupation).
Au delà du réglage du PID, mon souhait est aussi de faire du prédictif pour anticiper un changement de consigne planifié et évaluer le temps pour atteindre une consigne donnée le matin pour une heure précise. Et tant qu'à faire, automatiser un peu l'apprentissage des paramètres (avec les méthodes que tu cites ou de l'affinage empirique) car là je me fais la main sur le salon, mais une fois au point je compte passer aux autres pièces et si je pouvais éviter de les paramétrer une par une à la mano...

Du coup arrêtez moi si je suis hors sujet et si je pourris le topic. Peut-être que cette discussion a sa place ailleurs... (ou nul part...)