usda-hass-config/custom_components/pid_controller/pidcontroller.py

214 lines
5.6 KiB
Python
Raw Permalink Normal View History

2024-10-01 14:06:39 +00:00
#
# Copyright (c) 2022, Diogo Silva "Soloam"
# Creative Commons BY-NC-SA 4.0 International Public License
# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/)
#
"""
PID Controller.
For more details about this sensor, please refer to the documentation at
https://github.com/soloam/ha-pid-controller/
"""
import time
# pylint: disable=invalid-name
class PIDController:
"""PID Controller"""
WARMUP_STAGE = 3
def __init__(self, P=0.2, I=0.0, D=0.0, logger=None):
self._logger = logger
self._set_point = 0
self._windup = (None, None)
self._output = 0.0
self._kp = P
self._ki = I
self._kd = D
self._p_term = 0.0
self._i_term = 0.0
self._d_term = 0.0
self._sample_time = None
self._last_output = None
self._last_input = None
self._last_time = None
self.reset_pid()
def reset_pid(self):
self._p_term = 0.0
self._i_term = 0.0
self._d_term = 0.0
self._sample_time = None
self._last_output = None
self._last_input = None
self._last_time = None
def update(self, feedback_value, in_time=None):
"""Calculates PID value for given reference feedback"""
current_time = in_time if in_time is not None else self.current_time()
if self._last_time is None:
self._last_time = current_time
# Fill PID information
delta_time = current_time - self._last_time
if not delta_time:
delta_time = 1e-16
elif delta_time < 0:
return
# Return last output if sample time not met
if (
self._sample_time is not None
and self._last_output is not None
and delta_time < self._sample_time
):
return self._last_output
# Calculate error
error = self._set_point - feedback_value
last_error = self._set_point - (
self._last_input if self._last_input is not None else self._set_point
)
# Calculate delta error
delta_error = error - last_error
# Calculate P
self._p_term = self._kp * error
# Calculate I and avoids Sturation
if self._last_output is None or (
self._last_output > 0 and self._last_output < 100
):
self._i_term += self._ki * error * delta_time
self._i_term = self.clamp_value(self._i_term, self._windup)
# Calculate D
self._d_term = self._kd * delta_error / delta_time
# Compute final output
self._output = self._p_term + self._i_term + self._d_term
self._output = self.clamp_value(self._output, (0, 100))
# Keep Track
self._last_output = self._output
self._last_input = feedback_value
self._last_time = current_time
@property
def kp(self):
"""Aggressively the PID reacts to the current error with setting Proportional Gain"""
return self._kp
@kp.setter
def kp(self, value):
self._kp = value
@property
def ki(self):
"""Aggressively the PID reacts to the current error with setting Integral Gain"""
return self._ki
@ki.setter
def ki(self, value):
self._ki = value
@property
def kd(self):
"""Determines how aggressively the PID reacts to the current
error with setting Derivative Gain"""
return self._kd
@kd.setter
def kd(self, value):
self._kd = value
@property
def set_point(self):
"""The target point to the PID"""
return self._set_point
@set_point.setter
def set_point(self, value):
self._set_point = value
@property
def windup(self):
"""Integral windup, also known as integrator windup or reset windup,
refers to the situation in a PID feedback controller where
a large change in setpoint occurs (say a positive change)
and the integral terms accumulates a significant error
during the rise (windup), thus overshooting and continuing
to increase as this accumulated error is unwound
(offset by errors in the other direction).
The specific problem is the excess overshooting.
"""
return self._windup
@windup.setter
def windup(self, value):
self._windup = (-value, value)
@property
def sample_time(self):
"""PID that should be updated at a regular interval.
Based on a pre-determined sampe time, the PID decides if it should compute or
return immediately.
"""
return self._sample_time
@sample_time.setter
def sample_time(self, value):
self._sample_time = value
@property
def p(self):
return self._p_term
@property
def i(self):
return self._i_term
@property
def d(self):
return self._d_term
@property
def output(self):
"""PID result"""
return self._output
def log(self, message):
if not self._logger:
return
self._logger.warning(message)
def current_time(self):
try:
ret_time = time.monotonic()
except AttributeError:
ret_time = time.time()
return ret_time
def clamp_value(self, value, limits):
lower, upper = limits
if value is None:
return None
elif not lower and not upper:
return value
elif (upper is not None) and (value > upper):
return upper
elif (lower is not None) and (value < lower):
return lower
return value