Algo implementation and tests
This commit is contained in:
@@ -66,8 +66,6 @@ async def async_setup_entry(
|
||||
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
|
||||
|
||||
async_add_entities([entity], True)
|
||||
# No more needed
|
||||
# VersatileThermostat.add_entity(entry.entry_id, entity)
|
||||
|
||||
# Add services
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
66
custom_components/versatile_thermostat/pi_algorithm.py
Normal file
66
custom_components/versatile_thermostat/pi_algorithm.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" The PI algorithm implementation """
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class PITemperatureRegulator:
|
||||
""" A class implementing a PI Algorithm
|
||||
PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
|
||||
- offset = kp * error + ki * accumulated_error
|
||||
|
||||
To use it you must:
|
||||
- instanciate the class and gives the algorithm parameters: kp, ki, offset_max, stabilization_threshold, accumulated_error_threshold
|
||||
- call calculate_regulated_temperature with the internal and external temperature
|
||||
- call set_target_temp when the target temperature change.
|
||||
"""
|
||||
|
||||
def __init__(self, target_temp, kp, ki, k_ext, offset_max, stabilization_threshold, accumulated_error_threshold):
|
||||
self.target_temp = target_temp
|
||||
self.kp = kp # proportionnel gain
|
||||
self.ki = ki # integral gain
|
||||
self.k_ext = k_ext # exterior gain
|
||||
self.offset_max = offset_max
|
||||
self.stabilization_threshold = stabilization_threshold
|
||||
self.accumulated_error = 0
|
||||
self.accumulated_error_threshold = accumulated_error_threshold
|
||||
|
||||
def set_target_temp(self, target_temp):
|
||||
""" Set the new target_temp"""
|
||||
self.target_temp = target_temp
|
||||
self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(self, internal_temp, external_temp): # pylint: disable=unused-argument
|
||||
""" Calculate a new target_temp given some temperature"""
|
||||
# Calculate the error factor (P)
|
||||
error = self.target_temp - internal_temp
|
||||
|
||||
# Calculate the sum of error (I)
|
||||
self.accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self.accumulated_error = min(self.accumulated_error_threshold, max(-self.accumulated_error_threshold, self.accumulated_error))
|
||||
|
||||
# Calculate the offset (proportionnel + intégral)
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
|
||||
# Calculate the exterior offset
|
||||
offset_ext = self.k_ext * (self.target_temp - external_temp)
|
||||
|
||||
# Capping of offset_ext
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
|
||||
# If temperature is near the target_temp, reset the accumulated_error
|
||||
if abs(error) < self.stabilization_threshold:
|
||||
_LOGGER.debug("Stabilisation")
|
||||
self.accumulated_error = 0
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug("PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error, self.accumulated_error, offset, offset_ext, self.target_temp, result)
|
||||
|
||||
return result
|
||||
171
tests/test_pi.py
Normal file
171
tests/test_pi.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" Tests de PI algorithm used for auto-regulation """
|
||||
|
||||
from custom_components.versatile_thermostat.pi_algorithm import PITemperatureRegulator
|
||||
|
||||
def test_pi_algorithm_basics():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
|
||||
|
||||
assert the_algo
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
# Test the accumulator threshold effect and offset_max
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22
|
||||
# Will keep infinitly 22.0
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
|
||||
|
||||
def test_pi_algorithm_very_light():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
|
||||
def test_pi_algorithm_medium():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.5, ki=0.1, k_ext=0.1, offset_max=3, stabilization_threshold=0.1, accumulated_error_threshold=30)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.8
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.4
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 21.6
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 21.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 22.0
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 22.6
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 22.8
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
# Test the error acculation effect
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.1
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.6
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.9
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
|
||||
def test_pi_algorithm_strong():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.6, ki=0.2, k_ext=0.2, offset_max=4, stabilization_threshold=0.1, accumulated_error_threshold=40)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 24.0
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.2
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.6
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.2
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.0
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 23.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 24
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
# Test the error acculation effect
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.2
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.4
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.8
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
@@ -1,5 +1,6 @@
|
||||
""" Test the TPI algorithm """
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@@ -37,7 +38,7 @@ async def test_tpi_calculation(
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
Reference in New Issue
Block a user