From b323d676dcf3298b01e602d560097ccb7ea50346 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 29 Oct 2023 16:43:07 +0000 Subject: [PATCH] Algo implementation and tests --- .../versatile_thermostat/climate.py | 2 - .../versatile_thermostat/pi_algorithm.py | 66 +++++++ tests/test_pi.py | 171 ++++++++++++++++++ tests/test_tpi.py | 3 +- 4 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 custom_components/versatile_thermostat/pi_algorithm.py create mode 100644 tests/test_pi.py diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 103a36d..d614d08 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -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() diff --git a/custom_components/versatile_thermostat/pi_algorithm.py b/custom_components/versatile_thermostat/pi_algorithm.py new file mode 100644 index 0000000..60f0d30 --- /dev/null +++ b/custom_components/versatile_thermostat/pi_algorithm.py @@ -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 diff --git a/tests/test_pi.py b/tests/test_pi.py new file mode 100644 index 0000000..6423381 --- /dev/null +++ b/tests/test_pi.py @@ -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 \ No newline at end of file diff --git a/tests/test_tpi.py b/tests/test_tpi.py index b27b39f..ca0d0b6 100644 --- a/tests/test_tpi.py +++ b/tests/test_tpi.py @@ -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