diff --git a/plugin.py b/plugin.py index c661b86..18de9de 100644 --- a/plugin.py +++ b/plugin.py @@ -27,7 +27,7 @@ - @@ -68,10 +68,12 @@ class BasePlugin: 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 = 0.2 # 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 +95,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 @@ -162,6 +164,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: @@ -169,10 +175,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 @@ -203,6 +212,9 @@ def onStart(self): else: Domoticz.Error("Error reading Mode5 parameters") + # 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), # just delete the relevant "-InternalVariables" user variable Domoticz GUI and restart plugin @@ -244,31 +256,31 @@ 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)): + 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 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) @@ -276,13 +288,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) @@ -290,18 +302,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) @@ -318,14 +330,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) @@ -356,19 +368,20 @@ 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 + 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") - if power == 0: + if power <= 0: self.switchHeat(False) - Domoticz.Debug("No heating requested !") + 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: @@ -379,12 +392,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,7 +411,7 @@ def AutoCallib(self): # 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) / @@ -412,7 +424,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) / @@ -456,7 +468,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 @@ -464,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) @@ -595,7 +609,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: