Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions custom_components/smart_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@
vol.Optional(const.CONF_LOOKBACK, default=const.DEFAULT_LOOKBACK): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(const.CONF_DEBUG, default=False): cv.boolean,
vol.Optional(const.CONF_PERIOD_FOR_DERIVATIVE_CALCULATION, default=const.DEFAULT_PERIOD_FOR_DERIVATIVE_CALCULATION): vol.All(
cv.time_period, cv.positive_timedelta),
}
)

Expand Down Expand Up @@ -186,6 +188,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
'autotune': config.get(const.CONF_AUTOTUNE),
'noiseband': config.get(const.CONF_NOISEBAND),
'lookback': config.get(const.CONF_LOOKBACK),
'period_for_derivative_calculation': config.get(const.CONF_PERIOD_FOR_DERIVATIVE_CALCULATION),
const.CONF_DEBUG: config.get(const.CONF_DEBUG),
}

Expand Down Expand Up @@ -342,6 +345,7 @@ def __init__(self, **kwargs):
self._time_changed = 0
self._last_sensor_update = time.time()
self._last_ext_sensor_update = time.time()
self._period_for_derivative_calculation = kwargs.get('period_for_derivative_calculation')
if self._autotune != "none":
self._pid_controller = None
self._pid_autotune = pid_controller.PIDAutotune(self._difference, self._lookback,
Expand All @@ -357,7 +361,7 @@ def __init__(self, **kwargs):
self._pid_controller = pid_controller.PID(self._kp, self._ki, self._kd, self._ke,
self._min_out, self._max_out,
self._sampling_period, self._cold_tolerance,
self._hot_tolerance)
self._hot_tolerance, self._period_for_derivative_calculation)
self._pid_controller.mode = "AUTO"

async def async_added_to_hass(self):
Expand Down Expand Up @@ -1087,7 +1091,8 @@ async def calc_output(self):
self._ke, self._min_out,
self._max_out, self._sampling_period,
self._cold_tolerance,
self._hot_tolerance)
self._hot_tolerance,
self._period_for_derivative_calculation)
self._autotune = "none"
self._control_output = self._pid_autotune.output
self._p = self._i = self._d = error = self._dt = 0
Expand Down
2 changes: 2 additions & 0 deletions custom_components/smart_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
DEFAULT_SENSOR_STALL = '06:00:00'
DEFAULT_OUTPUT_SAFETY = 5.0
DEFAULT_PRESET_SYNC_MODE = "none"
DEFAULT_PERIOD_FOR_DERIVATIVE_CALCULATION = '01:00:00'

CONF_HEATER = "heater"
CONF_COOLER = "cooler"
Expand Down Expand Up @@ -60,3 +61,4 @@
CONF_NOISEBAND = "noiseband"
CONF_LOOKBACK = "lookback"
CONF_DEBUG = 'debug'
CONF_PERIOD_FOR_DERIVATIVE_CALCULATION = 'period_for_derivative_calculation'
60 changes: 55 additions & 5 deletions custom_components/smart_thermostat/pid_controller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import math
import logging
from time import time
Expand All @@ -12,7 +13,7 @@ class PID:
error: float

def __init__(self, kp, ki, kd, ke=0, out_min=float('-inf'), out_max=float('+inf'),
sampling_period=0, cold_tolerance=0.3, hot_tolerance=0.3):
sampling_period=0, cold_tolerance=0.3, hot_tolerance=0.3, period_for_derivative_calculation=3600.0):
"""A proportional-integral-derivative controller.
:param kp: Proportional coefficient.
:type kp: float
Expand Down Expand Up @@ -70,6 +71,13 @@ def __init__(self, kp, ki, kd, ke=0, out_min=float('-inf'), out_max=float('+inf'
self._sampling_period = sampling_period
self._cold_tolerance = cold_tolerance
self._hot_tolerance = hot_tolerance
self._period_for_derivative_calculation = period_for_derivative_calculation

self._input_point_for_derivative_calculation = 0
self._input_point_timestamp_for_derivative_calculation = time()
self._previous_input_points_for_derivative_calculation = []
self._previous_input_points_timestamp_for_derivative_calculation = []
self._is_this_first_input = True

@property
def mode(self):
Expand Down Expand Up @@ -140,6 +148,9 @@ def set_pid_param(self, kp=None, ki=None, kd=None, ke=None):
if ke is not None and isinstance(ke, (int, float)):
self._Ke = ke

def elapsed_time(self, timestamp: float):
return time() - timestamp

def clear_samples(self):
"""Clear the samples values and timestamp to restart PID from clean state after
a switch off of the thermostat"""
Expand Down Expand Up @@ -173,6 +184,29 @@ def calc(self, input_val, set_point, input_time=None, last_input_time=None, ext_
self._last_input_time = self._input_time
self._last_output = self._output

self._previous_input_points_timestamp_for_derivative_calculation.append(input_time)
self._previous_input_points_for_derivative_calculation.append(input_val)

seconds_elapsed_since_last_derivative_input = datetime.timedelta(seconds=self.elapsed_time(
self._input_point_timestamp_for_derivative_calculation))
idx = 999999
if seconds_elapsed_since_last_derivative_input > self._period_for_derivative_calculation.total_seconds():
for i, timestamp in enumerate(self._previous_input_points_timestamp_for_derivative_calculation):
seconds_elapsed = self.elapsed_time(timestamp)
if seconds_elapsed >= self._period_for_derivative_calculation:
idx = i
break
new_input_time = self._previous_input_points_timestamp_for_derivative_calculation[idx]
new_input_val = self._previous_input_points_for_derivative_calculation[idx]
self._input_point_timestamp_for_derivative_calculation = new_input_time
self._input_point_for_derivative_calculation = new_input_val
if self._is_this_first_input:
self._is_this_first_input = False
self._input_point_for_derivative_calculation = input_val
self._input_point_timestamp_for_derivative_calculation = input_time

self._previous_input_points_timestamp_for_derivative_calculation = self._previous_input_points_timestamp_for_derivative_calculation[:idx]
self._previous_input_points_for_derivative_calculation = self._previous_input_points_for_derivative_calculation[:idx]
# Refresh with actual values
self._input = input_val
if self._sampling_period == 0:
Expand Down Expand Up @@ -209,6 +243,7 @@ def calc(self, input_val, set_point, input_time=None, last_input_time=None, ext_
else:
self._dext = 0

error_for_derivative = input_val - self._input_point_for_derivative_calculation
# In order to prevent windup, only integrate if the process is not saturated and set point
# is stable
if self._out_min < self._last_output < self._out_max and \
Expand All @@ -217,16 +252,31 @@ def calc(self, input_val, set_point, input_time=None, last_input_time=None, ext_
self._integral = max(min(self._integral, self._out_max), self._out_min)

self._proportional = self._Kp * self._error
if self._dt != 0:
self._derivative = -(self._Kd * self._input_diff) / self._dt
else:
self._derivative = 0.0
self._derivative = -(self._Kd * error_for_derivative) / self._period_for_derivative_calculation.total_seconds()
# Compensate losses due to external temperature
self._external = self._Ke * self._dext

# Compute PID Output
output = self._proportional + self._integral + self._derivative + self._external
self._output = max(min(output, self._out_max), self._out_min)

# Log some debug info
# _LOGGER.debug('P: %.2f', self._proportional)
# _LOGGER.debug('I: %.2f', self._integral)
# _LOGGER.debug('D: %.2f', self._derivative)
# _LOGGER.debug('E: %.2f', self._external)
# _LOGGER.debug('output: %.2f', self._output)
# _LOGGER.debug('[mihadebug] input_point_for_derivative_calculation, input_val: %f %f', self._input_point_for_derivative_calculation, input_val)
# _LOGGER.debug('[mihadebug] input_point_timestamp_for_derivative_calculation: %f', self._input_point_timestamp_for_derivative_calculation)
# _LOGGER.debug('[mihadebug] error_for_derivative: %f', error_for_derivative)
# _LOGGER.debug('[mihadebug] dt_for_derivative: %f', dt_for_derivative)
# _LOGGER.warning(f"self._dext = set_point - ext_temp,\n{self._dext} = {set_point} * {ext_temp}")
# _LOGGER.warning(f"self._external = self._Ke * self._dext,\n{self._external} = {self._Ke} * {self._dext}")
# try:
# _LOGGER.warning('-(self._Kd * error_for_derivative) / dt = -(%1.2f * %1.2f) / %4.0f = %1.2f', self._Kd, error_for_derivative, dt_for_derivative, -(self._Kd * error_for_derivative) / dt_for_derivative)
# except ZeroDivisionError:
# pass

return self._output, True


Expand Down