From 40a8f1af4e09f9318c467c9e159661e884059723 Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Wed, 27 Jan 2021 20:44:54 +0100 Subject: [PATCH 1/7] Add power device --- plugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 7adceb8..318c2f8 100644 --- a/plugin.py +++ b/plugin.py @@ -163,6 +163,10 @@ def onStart(self): 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 7 not in Devices: + Domoticz.Device(Name="Power", Unit=7, Type=243, Subtype=6, Used=0, Description="Power percentage calculated this period").Create() + devicecreated.append(deviceparam(7, 0, "0")) # default is 0 percent + # if any device has been created in onStart(), now is time to update its defaults for device in devicecreated: @@ -245,7 +249,7 @@ def onHeartbeat(self): now = datetime.now() # fool proof checking.... based on users feedback - if not all(device in Devices for device in (1,2,3,4,5,6)): + if not all(device in Devices for device in (1,2,3,4,5,6,7)): Domoticz.Error("one or more devices required by the plugin is/are missing, please check domoticz device creation settings and restart !") return @@ -359,6 +363,7 @@ def AutoMode(self): "Calculated power is {}, applying minimum power of {}".format(power, self.minheatpower), "Verbose") power = self.minheatpower + Devices[7].Update(nValue=Devices[7].nValue, sValue=str(power), TimedOut=False) heatduration = round(power * self.calculate_period / 100) self.WriteLog("Calculation: Power = {} -> heat duration = {} minutes".format(power, heatduration), "Verbose") From e8d3a21ac0df3a7e6e62fdd4e5dcbb958f3935d4 Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Thu, 28 Jan 2021 11:24:26 +0100 Subject: [PATCH 2/7] Learn from mistakes too + power calculation at 2 digits --- plugin.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/plugin.py b/plugin.py index 318c2f8..5cf214e 100644 --- a/plugin.py +++ b/plugin.py @@ -71,7 +71,7 @@ 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.deltamax = 0.1 # 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 @@ -336,21 +336,22 @@ def AutoMode(self): self.WriteLog("Temperatures: Inside = {} / Outside = {}".format(self.intemp, self.outtemp), "Verbose") + if self.learn: + self.AutoCallib() + else: + self.learn = True + if self.intemp > self.setpoint + self.deltamax: self.WriteLog("Temperature exceeds setpoint", "Verbose") overshoot = True power = 0 else: overshoot = False - if self.learn: - self.AutoCallib() - else: - self.learn = True if self.outtemp is None: - power = round((self.setpoint - self.intemp) * self.Internals["ConstC"], 1) + power = round((self.setpoint - self.intemp) * self.Internals["ConstC"], 2) else: power = round((self.setpoint - self.intemp) * self.Internals["ConstC"] + - (self.setpoint - self.outtemp) * self.Internals["ConstT"], 1) + (self.setpoint - self.outtemp) * self.Internals["ConstT"], 2) if power < 0: power = 0 # lower limit @@ -364,7 +365,7 @@ def AutoMode(self): power = self.minheatpower Devices[7].Update(nValue=Devices[7].nValue, sValue=str(power), TimedOut=False) - heatduration = round(power * self.calculate_period / 100) + heatduration = round(power * self.calculate_period / 100, 1) self.WriteLog("Calculation: Power = {} -> heat duration = {} minutes".format(power, heatduration), "Verbose") if power == 0: @@ -406,7 +407,7 @@ def AutoCallib(self): (self.calculate_period * 60)))) self.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'] + 1), 2) self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, 50) self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") elif (self.outtemp is not None and self.Internals['LastOutT'] is not None) and \ @@ -419,7 +420,7 @@ def AutoCallib(self): (self.calculate_period * 60)))) self.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'] + 1), 2) self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, 50) self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") From c2756b2c41b20ed165a7353de5892aca6fafa25c Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Fri, 29 Jan 2021 18:57:43 +0100 Subject: [PATCH 3/7] precision and attempt at fixing running into safety --- history.txt | 8 +++++++ plugin.py | 62 ++++++++++++++++++++++++++--------------------------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/history.txt b/history.txt index 3169968..013cb7d 100644 --- a/history.txt +++ b/history.txt @@ -58,3 +58,11 @@ Version history Smart Virtual Thermostat: minor code cleanup + move version history from plugin.py to history.txt file 0.4.10 (November 25, 2020): bugfix on setting minimum force mode duration (thanks to domoticz forum user mash47) +0.4.11 (January 29, 2021): rrozema + bugfixes for: + (1) also learn when overshot (to improve learning rate) + (2) always use the same time per heartbeat (fixes inprecision in time calculations) + (3) increase precision of learn values to 2 decimals (fixes rounding issues) + (4) also learn when Tin matches setpoint (fixes we only go up, never down) + (5) increased maxdelta so we don't cut off the learning process to soon (fixes power going to 0 + all the time, which causes uncomfortable long times without heating) diff --git a/plugin.py b/plugin.py index 5cf214e..9c0c324 100644 --- a/plugin.py +++ b/plugin.py @@ -1,13 +1,13 @@ """ Smart Virtual Thermostat python plugin for Domoticz -Author: Logread, +Author: Logread, rrozema adapted from the Vera plugin by Antor, see: http://www.antor.fr/apps/smart-virtual-thermostat-eng-2/?lang=en https://github.com/AntorFr/SmartVT -Version: 0.4.10 (November 25, 2020) - see history.txt for versions history +Version: 0.4.11 (November 25, 2020) - see history.txt for versions history """ """ - +

Smart Virtual Thermostat


Easily implement in Domoticz an advanced virtual thermostat based on time modulation
@@ -27,7 +27,7 @@ - @@ -68,10 +68,11 @@ class BasePlugin: def __init__(self): + self.now = datetime.now() 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.1 # allowed temp excess over setpoint temperature + self.deltamax = 2.0 # 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 @@ -93,12 +94,12 @@ def __init__(self): self.heat = False self.pause = False self.pauserequested = False - self.pauserequestchangedtime = datetime.now() + self.pauserequestchangedtime = self.now self.forced = False self.intemp = 20.0 self.outtemp = 20.0 self.setpoint = 20.0 - self.endheat = datetime.now() + self.endheat = self.now self.nextcalc = self.endheat self.lastcalc = self.endheat self.nextupdate = self.endheat @@ -246,7 +247,7 @@ def onCommand(self, Unit, Command, Level, Color): def onHeartbeat(self): - now = datetime.now() + self.now = datetime.now() # fool proof checking.... based on users feedback if not all(device in Devices for device in (1,2,3,4,5,6,7)): @@ -256,21 +257,21 @@ def onHeartbeat(self): 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 + self.endheat = self.now Domoticz.Debug("Switching heat Off !") self.switchHeat(False) elif Devices[1].sValue == "20": # Thermostat is in forced mode if self.forced: - if self.endheat <= now: + if self.endheat <= self.now: self.forced = False - self.endheat = now + self.endheat = self.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) + self.endheat = self.now + timedelta(minutes=self.forcedduration) Domoticz.Debug("Forced mode On !") self.switchHeat(True) @@ -278,13 +279,13 @@ def onHeartbeat(self): 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 + self.endheat = self.now + self.nextcalc = self.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 + elif (self.endheat <= self.now or self.pause) and self.heat: # heat cycle is over + self.endheat = self.now self.heat = False if self.Internals['LastPwr'] < 100: self.switchHeat(False) @@ -292,18 +293,18 @@ def onHeartbeat(self): # 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: + if self.pauserequestchangedtime + timedelta(minutes=self.pauseoffdelay) <= self.now: self.WriteLog("Pause is now Off", "Status") 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: + if self.pauserequestchangedtime + timedelta(minutes=self.pauseondelay) <= self.now: self.WriteLog("Pause is now On", "Status") self.pause = True self.switchHeat(False) - elif (self.nextcalc <= now) and not self.pause: # we start a new calculation - self.nextcalc = now + timedelta(minutes=self.calculate_period) + elif (self.nextcalc <= self.now) and not self.pause: # we start a new calculation + self.nextcalc = self.now + timedelta(minutes=self.calculate_period) self.WriteLog("Next calculation time will be : " + str(self.nextcalc), "Verbose") # make current setpoint used in calculation reflect the select mode (10= normal, 20 = economy) @@ -320,14 +321,14 @@ def onHeartbeat(self): # make sure we switch off heating if there was an error with reading the temp self.switchHeat(False) - if self.nexttemps <= now: + if self.nexttemps <= self.now: # call the Domoticz json API for a temperature devices update, to get the lastest temps (and avoid the # connection time out time after 10mins that floods domoticz logs in versions of domoticz since spring 2018) self.readTemps() # 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"])) + if self.nextupdate <= self.now: + self.nextupdate = self.now + timedelta(minutes=int(Settings["SensorTimeout"])) Devices[4].Update(nValue=0, sValue=Devices[4].sValue) Devices[5].Update(nValue=0, sValue=Devices[5].sValue) @@ -372,7 +373,7 @@ def AutoMode(self): self.switchHeat(False) Domoticz.Debug("No heating requested !") else: - self.endheat = datetime.now() + timedelta(minutes=heatduration) + self.endheat = self.now + timedelta(minutes=heatduration) Domoticz.Debug("End Heat time = " + str(self.endheat)) self.switchHeat(True) if self.Internals["ALStatus"] < 2: @@ -383,12 +384,11 @@ def AutoMode(self): self.Internals['ALStatus'] = 1 self.saveUserVar() # update user variables with latest learning - self.lastcalc = datetime.now() + self.lastcalc = self.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 @@ -399,11 +399,11 @@ def AutoCallib(self): # 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']: + elif self.intemp >= self.Internals['LastInT'] and self.Internals['LastInT'] <= self.Internals['LastSetPoint'] + self.deltamax: # learning ConstC ConstC = (self.Internals['ConstC'] * ((self.Internals['LastSetPoint'] - self.Internals['LastInT']) / (self.intemp - self.Internals['LastInT']) * - (timedelta.total_seconds(now - self.lastcalc) / + (timedelta.total_seconds(self.now - self.lastcalc) / (self.calculate_period * 60)))) self.WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose") self.Internals['ConstC'] = round((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / @@ -416,7 +416,7 @@ def AutoCallib(self): ConstT = (self.Internals['ConstT'] + ((self.Internals['LastSetPoint'] - self.intemp) / (self.Internals['LastSetPoint'] - self.Internals['LastOutT']) * self.Internals['ConstC'] * - (timedelta.total_seconds(now - self.lastcalc) / + (timedelta.total_seconds(self.now - self.lastcalc) / (self.calculate_period * 60)))) self.WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose") self.Internals['ConstT'] = round((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / @@ -460,7 +460,7 @@ def switchHeat(self, switch): def readTemps(self): # set update flag for next temp update - self.nexttemps = datetime.now() + timedelta(minutes=5) + self.nexttemps = self.now + timedelta(minutes=5) # fetch all the devices from the API and scan for sensors noerror = True @@ -599,7 +599,7 @@ def LastUpdate(datestring): result = datetime(*(time.strptime(datestring, dateformat)[0:6])) return result - timedout = LastUpdate(datestring) + timedelta(minutes=int(Settings["SensorTimeout"])) < datetime.now() + timedout = LastUpdate(datestring) + timedelta(minutes=int(Settings["SensorTimeout"])) < self.now # handle logging of time outs... only log when status changes (less clutter in logs) if timedout: From 71f6c97ff1e4f3472afbb854c1739a6f66f3b8fe Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Fri, 29 Jan 2021 22:17:00 +0100 Subject: [PATCH 4/7] if intemp is over setpoint, don't apply more power Also avoid divide by zero when intemp equals last in temp --- plugin.py | 61 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/plugin.py b/plugin.py index 9c0c324..2591310 100644 --- a/plugin.py +++ b/plugin.py @@ -4,7 +4,7 @@ adapted from the Vera plugin by Antor, see: http://www.antor.fr/apps/smart-virtual-thermostat-eng-2/?lang=en https://github.com/AntorFr/SmartVT -Version: 0.4.11 (November 25, 2020) - see history.txt for versions history +Version: 0.4.11 (January 29, 2020) - see history.txt for versions history """ """ @@ -341,7 +341,7 @@ def AutoMode(self): self.AutoCallib() else: self.learn = True - + if self.intemp > self.setpoint + self.deltamax: self.WriteLog("Temperature exceeds setpoint", "Verbose") overshoot = True @@ -349,10 +349,10 @@ def AutoMode(self): else: overshoot = False if self.outtemp is None: - power = round((self.setpoint - self.intemp) * self.Internals["ConstC"], 2) + power = round((self.setpoint - min(self.intemp, self.setpoint)) * self.Internals["ConstC"], 2) else: - power = round((self.setpoint - self.intemp) * self.Internals["ConstC"] + - (self.setpoint - self.outtemp) * self.Internals["ConstT"], 2) + power = round((self.setpoint - min(self.intemp, self.setpoint)) * self.Internals["ConstC"] + + (self.setpoint - min(self.outtemp, self.setpoint)) * self.Internals["ConstT"], 2) if power < 0: power = 0 # lower limit @@ -360,7 +360,7 @@ def AutoMode(self): power = 100 # upper limit # apply minimum power as required - if power <= self.minheatpower and (Parameters["Mode4"] == "Forced" or not overshoot): + if power < self.minheatpower and (Parameters["Mode4"] == "Forced" or not overshoot): self.WriteLog( "Calculated power is {}, applying minimum power of {}".format(power, self.minheatpower), "Verbose") power = self.minheatpower @@ -399,30 +399,31 @@ def AutoCallib(self): # 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['LastInT'] <= self.Internals['LastSetPoint'] + self.deltamax: - # learning ConstC - ConstC = (self.Internals['ConstC'] * ((self.Internals['LastSetPoint'] - self.Internals['LastInT']) / - (self.intemp - self.Internals['LastInT']) * - (timedelta.total_seconds(self.now - self.lastcalc) / - (self.calculate_period * 60)))) - self.WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose") - self.Internals['ConstC'] = round((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / - (self.Internals['nbCC'] + 1), 2) - self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, 50) - self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") - elif (self.outtemp is not None and self.Internals['LastOutT'] 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(self.now - self.lastcalc) / - (self.calculate_period * 60)))) - self.WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose") - self.Internals['ConstT'] = round((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / - (self.Internals['nbCT'] + 1), 2) - self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, 50) - self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") + elif self.Internals['LastSetPoint'] > self.Internals['LastInT']: + if self.intemp > self.Internals['LastInT']: + # learning ConstC + ConstC = (self.Internals['ConstC'] * ((self.Internals['LastSetPoint'] - self.Internals['LastInT']) / + (self.intemp - self.Internals['LastInT']) * + (timedelta.total_seconds(self.now - self.lastcalc) / + (self.calculate_period * 60)))) + self.WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose") + self.Internals['ConstC'] = round((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / + (self.Internals['nbCC'] + 1), 2) + self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, 50) + self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") + elif (self.outtemp is not None and self.Internals['LastOutT'] is not None): + if 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(self.now - self.lastcalc) / + (self.calculate_period * 60)))) + self.WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose") + self.Internals['ConstT'] = round((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / + (self.Internals['nbCT'] + 1), 2) + self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, 50) + self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") def switchHeat(self, switch): From 42ecf5d09b1963ed730736b37f7ac2935d769ee8 Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Sat, 30 Jan 2021 09:58:08 +0100 Subject: [PATCH 5/7] further improvements - remove rounding for constC and ConstT - always learn over a period of 2 days - allow for weighted averaging on temp sensors (if a sensor is listed twice that sensor will weigh twice in the average temperature) --- plugin.py | 57 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/plugin.py b/plugin.py index 2591310..a0394ee 100644 --- a/plugin.py +++ b/plugin.py @@ -71,6 +71,7 @@ def __init__(self): self.now = datetime.now() self.debug = False self.calculate_period = 30 # Time in minutes between two calculations (cycle) + self.num_periods = 50 # The number of periods over which we learn the system characteristics. self.minheatpower = 0 # if heating is needed, minimum heat power (in % of calculation period) self.deltamax = 2.0 # allowed temp excess over setpoint temperature self.pauseondelay = 2 # time between pause sensor actuation and actual pause @@ -175,10 +176,13 @@ def onStart(self): # build lists of sensors and switches self.InTempSensors = parseCSV(Parameters["Mode1"]) + self.InTempSensors.sort() self.WriteLog("Inside Temperature sensors = {}".format(self.InTempSensors), "Verbose") self.OutTempSensors = parseCSV(Parameters["Mode2"]) + self.OutTempSensors.sort() self.WriteLog("Outside Temperature sensors = {}".format(self.OutTempSensors), "Verbose") self.Heaters = parseCSV(Parameters["Mode3"]) + self.Heaters.sort() self.WriteLog("Heaters = {}".format(self.Heaters), "Verbose") # build dict of status of all temp sensors to be used when handling timeouts @@ -205,6 +209,9 @@ def onStart(self): else: Domoticz.Error("Error reading Mode5 parameters") + # calculate the number of periods if we want to average over a 2 days period. + self.num_periods = (2880 / self.calculate_period) + # 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 "-InternalVariables" user variable Domoticz GUI and restart plugin @@ -407,9 +414,9 @@ def AutoCallib(self): (timedelta.total_seconds(self.now - self.lastcalc) / (self.calculate_period * 60)))) self.WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose") - self.Internals['ConstC'] = round((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / - (self.Internals['nbCC'] + 1), 2) - self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, 50) + self.Internals['ConstC'] = ((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / + (self.Internals['nbCC'] + 1)) + self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, self.num_periods) self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") elif (self.outtemp is not None and self.Internals['LastOutT'] is not None): if self.Internals['LastSetPoint'] > self.Internals['LastOutT']: @@ -420,9 +427,9 @@ def AutoCallib(self): (timedelta.total_seconds(self.now - self.lastcalc) / (self.calculate_period * 60)))) self.WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose") - self.Internals['ConstT'] = round((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / - (self.Internals['nbCT'] + 1), 2) - self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, 50) + self.Internals['ConstT'] = ((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / + (self.Internals['nbCT'] + 1)) + self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, self.num_periods) self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") @@ -469,24 +476,26 @@ def readTemps(self): 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 self.SensorTimedOut(idx, device["Name"], device["LastUpdate"]): - listintemps.append(device["Temp"]) - 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 self.SensorTimedOut(idx, device["Name"], device["LastUpdate"]): - listouttemps.append(device["Temp"]) - else: - Domoticz.Error("device: {}-{} is not a Temperature sensor".format(device["idx"], device["Name"])) + for idx in self.InTempSensors: + for device in devicesAPI["result"]: # parse the devices for temperature sensors + if idx == int(device["idx"]): + if "Temp" in device: + Domoticz.Debug("device: {}-{} = {}".format(device["idx"], device["Name"], device["Temp"])) + # check temp sensor is not timed out + if not self.SensorTimedOut(idx, device["Name"], device["LastUpdate"]): + listintemps.append(device["Temp"]) + else: + Domoticz.Error("device: {}-{} is not a Temperature sensor".format(device["idx"], device["Name"])) + for idx in self.OutTempSensors: + for device in devicesAPI["result"]: # parse the devices for temperature sensors + if idx == int(device["idx"]): + if "Temp" in device: + Domoticz.Debug("device: {}-{} = {}".format(device["idx"], device["Name"], device["Temp"])) + # check temp sensor is not timed out + if not self.SensorTimedOut(idx, device["Name"], device["LastUpdate"]): + listouttemps.append(device["Temp"]) + else: + Domoticz.Error("device: {}-{} is not a Temperature sensor".format(device["idx"], device["Name"])) # calculate the average inside temperature nbtemps = len(listintemps) From 2320494beb898fd30567d2f8756786e73ae14d94 Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Thu, 4 Feb 2021 15:56:21 +0100 Subject: [PATCH 6/7] ConsT from in-out difference --- plugin.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/plugin.py b/plugin.py index a0394ee..f7f8d9c 100644 --- a/plugin.py +++ b/plugin.py @@ -209,8 +209,8 @@ def onStart(self): else: Domoticz.Error("Error reading Mode5 parameters") - # calculate the number of periods if we want to average over a 2 days period. - self.num_periods = (2880 / self.calculate_period) + # calculate the number of periods if we want to average over a 6 hours period. + self.num_periods = (360 / self.calculate_period) # loads persistent variables from dedicated user variable # note: to reset the thermostat to default values (i.e. ignore all past learning), @@ -359,7 +359,7 @@ def AutoMode(self): power = round((self.setpoint - min(self.intemp, self.setpoint)) * self.Internals["ConstC"], 2) else: power = round((self.setpoint - min(self.intemp, self.setpoint)) * self.Internals["ConstC"] + - (self.setpoint - min(self.outtemp, self.setpoint)) * self.Internals["ConstT"], 2) + (self.outtemp - self.intemp) * self.Internals["ConstT"], 2) if power < 0: power = 0 # lower limit @@ -376,9 +376,9 @@ def AutoMode(self): heatduration = round(power * self.calculate_period / 100, 1) self.WriteLog("Calculation: Power = {} -> heat duration = {} minutes".format(power, heatduration), "Verbose") - if power == 0: + if power <= 0: self.switchHeat(False) - Domoticz.Debug("No heating requested !") + Domoticz.Debug("No heating requested.") else: self.endheat = self.now + timedelta(minutes=heatduration) Domoticz.Debug("End Heat time = " + str(self.endheat)) @@ -406,33 +406,34 @@ def AutoCallib(self): # 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.Internals['LastSetPoint'] > self.Internals['LastInT']: - if self.intemp > self.Internals['LastInT']: + elif not self.Internals['LastInT'] > self.Internals['LastSetPoint'] + self.deltamax: + # heater was on, lets see what the result of our attempt was + if self.intemp != self.Internals['LastInT']: # if not spot on, calculate a new ConstC so we can do better next time. # learning ConstC ConstC = (self.Internals['ConstC'] * ((self.Internals['LastSetPoint'] - self.Internals['LastInT']) / (self.intemp - self.Internals['LastInT']) * (timedelta.total_seconds(self.now - self.lastcalc) / (self.calculate_period * 60)))) self.WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose") - self.Internals['ConstC'] = ((self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / - (self.Internals['nbCC'] + 1)) + self.Internals['ConstC'] = (self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / (self.Internals['nbCC'] + 1) self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, self.num_periods) self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") - elif (self.outtemp is not None and self.Internals['LastOutT'] is not None): - if self.Internals['LastSetPoint'] > self.Internals['LastOutT']: + elif not (self.outtemp is None or self.Internals['LastOutT'] is None): + # heater was not on and we have the required information: a reading of the current outside temperature + # plus its last read value. This is an opportunity to learn how the outside temperature affects our heatloss. + if self.Internals['LastInT'] != self.Internals['LastOutT']: # learning ConstT - ConstT = (self.Internals['ConstT'] + ((self.Internals['LastSetPoint'] - self.intemp) / - (self.Internals['LastSetPoint'] - self.Internals['LastOutT']) * + ConstT = (self.Internals['ConstT'] * ((self.Internals['LastInT'] - self.Internals['LastOutT']) / + (self.intemp - self.outemp) * self.Internals['ConstC'] * (timedelta.total_seconds(self.now - self.lastcalc) / (self.calculate_period * 60)))) self.WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose") - self.Internals['ConstT'] = ((self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / - (self.Internals['nbCT'] + 1)) + self.Internals['ConstT'] = (self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / (self.Internals['nbCT'] + 1) self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, self.num_periods) self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") - - + + def switchHeat(self, switch): # Build list of heater switches, with their current status, From f06e78d296806e62b89b2b60e5f63d3c3f36b929 Mon Sep 17 00:00:00 2001 From: rrozema <1747982+rrozema@users.noreply.github.com> Date: Thu, 4 Feb 2021 17:03:06 +0100 Subject: [PATCH 7/7] restore original ConstC and ConstT logic --- plugin.py | 77 ++++++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/plugin.py b/plugin.py index 638269b..18de9de 100644 --- a/plugin.py +++ b/plugin.py @@ -1,10 +1,10 @@ """ Smart Virtual Thermostat python plugin for Domoticz -Author: Logread, rrozema +Author: Logread, adapted from the Vera plugin by Antor, see: http://www.antor.fr/apps/smart-virtual-thermostat-eng-2/?lang=en https://github.com/AntorFr/SmartVT -Version: 0.4.11 (January 29, 2020) - see history.txt for versions history +Version: 0.4.10 (November 25, 2020) - see history.txt for versions history """ """ @@ -27,7 +27,7 @@ - @@ -346,22 +346,21 @@ def AutoMode(self): self.WriteLog("Temperatures: Inside = {} / Outside = {}".format(self.intemp, self.outtemp), "Verbose") - if self.learn: - self.AutoCallib() - else: - self.learn = True - if self.intemp > self.setpoint + self.deltamax: self.WriteLog("Temperature exceeds setpoint", "Verbose") overshoot = True power = 0 else: overshoot = False + if self.learn: + self.AutoCallib() + else: + self.learn = True if self.outtemp is None: - power = round((self.setpoint - min(self.intemp, self.setpoint)) * self.Internals["ConstC"], 2) + power = round((self.setpoint - self.intemp) * self.Internals["ConstC"], 1) else: - power = round((self.setpoint - min(self.intemp, self.setpoint)) * self.Internals["ConstC"] + - (self.outtemp - self.intemp) * self.Internals["ConstT"], 2) + power = round((self.setpoint - self.intemp) * self.Internals["ConstC"] + + (self.setpoint - self.outtemp) * self.Internals["ConstT"], 1) if power < 0: power = 0 # lower limit @@ -375,7 +374,7 @@ def AutoMode(self): power = self.minheatpower Devices[7].Update(nValue=Devices[7].nValue, sValue=str(power), TimedOut=False) - heatduration = round(power * self.calculate_period / 100, 1) + heatduration = round(power * self.calculate_period / 100) self.WriteLog("Calculation: Power = {} -> heat duration = {} minutes".format(power, heatduration), "Verbose") if power <= 0: @@ -408,34 +407,32 @@ def AutoCallib(self): # 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 not self.Internals['LastInT'] > self.Internals['LastSetPoint'] + self.deltamax: - # heater was on, lets see what the result of our attempt was - if self.intemp != self.Internals['LastInT']: # if not spot on, calculate a new ConstC so we can do better next time. - # learning ConstC - ConstC = (self.Internals['ConstC'] * ((self.Internals['LastSetPoint'] - self.Internals['LastInT']) / - (self.intemp - self.Internals['LastInT']) * - (timedelta.total_seconds(self.now - self.lastcalc) / - (self.calculate_period * 60)))) - self.WriteLog("New calc for ConstC = {}".format(ConstC), "Verbose") - self.Internals['ConstC'] = (self.Internals['ConstC'] * self.Internals['nbCC'] + ConstC) / (self.Internals['nbCC'] + 1) - self.Internals['nbCC'] = min(self.Internals['nbCC'] + 1, self.num_periods) - self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") - elif not (self.outtemp is None or self.Internals['LastOutT'] is None): - # heater was not on and we have the required information: a reading of the current outside temperature - # plus its last read value. This is an opportunity to learn how the outside temperature affects our heatloss. - if self.Internals['LastInT'] != self.Internals['LastOutT']: - # learning ConstT - ConstT = (self.Internals['ConstT'] * ((self.Internals['LastInT'] - self.Internals['LastOutT']) / - (self.intemp - self.outemp) * - self.Internals['ConstC'] * - (timedelta.total_seconds(self.now - self.lastcalc) / - (self.calculate_period * 60)))) - self.WriteLog("New calc for ConstT = {}".format(ConstT), "Verbose") - self.Internals['ConstT'] = (self.Internals['ConstT'] * self.Internals['nbCT'] + ConstT) / (self.Internals['nbCT'] + 1) - self.Internals['nbCT'] = min(self.Internals['nbCT'] + 1, self.num_periods) - self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") - - + 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(self.now - self.lastcalc) / + (self.calculate_period * 60)))) + self.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) + self.WriteLog("ConstC updated to {}".format(self.Internals['ConstC']), "Verbose") + elif (self.outtemp is not None and self.Internals['LastOutT'] 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(self.now - self.lastcalc) / + (self.calculate_period * 60)))) + self.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) + self.WriteLog("ConstT updated to {}".format(self.Internals['ConstT']), "Verbose") + + def switchHeat(self, switch): # Build list of heater switches, with their current status,