Implements regulation (tests ko)

This commit is contained in:
Jean-Marc Collin
2023-10-30 06:48:54 +00:00
parent 076d9eae24
commit dde622e632
6 changed files with 147 additions and 24 deletions

View File

@@ -198,7 +198,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._saved_hvac_mode = None
self._window_call_cancel = None
self._motion_call_cancel = None
self._cur_ext_temp = None
self._cur_temp = None
self._ac_mode = None
self._last_ext_temperature_mesure = None

View File

@@ -219,6 +219,33 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
class RegulationParamLight:
""" Light parameters for regulation"""
kp:float = 0.2
ki:float = 0.05
k_ext:float = 0.1
offset_max:float = 2
stabilization_threshold:float = 0.1
accumulated_error_threshold:float = 20
class RegulationParamMedium:
""" Medium parameters for regulation"""
kp:float = 0.5
ki:float = 0.1
k_ext:float = 0.1
offset_max:float = 3
stabilization_threshold:float = 0.1
accumulated_error_threshold:float = 30
class RegulationParamStrong:
""" Strong parameters for regulation"""
kp:float = 0.6
ki:float = 0.2
k_ext:float = 0.2
offset_max:float = 4
stabilization_threshold:float = 0.1
accumulated_error_threshold:float = 40
class EventType(Enum):
"""The event type that can be sent"""

View File

@@ -16,23 +16,27 @@ class PITemperatureRegulator:
- 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 __init__(self, target_temp: float, kp: float, ki: float, k_ext: float, offset_max: float, stabilization_threshold: float, accumulated_error_threshold: float):
self.target_temp:float = target_temp
self.kp:float = kp # proportionnel gain
self.ki:float = ki # integral gain
self.k_ext:float = k_ext # exterior gain
self.offset_max:float = offset_max
self.stabilization_threshold:float = stabilization_threshold
self.accumulated_error:float = 0
self.accumulated_error_threshold:float = 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
def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
""" Calculate a new target_temp given some temperature"""
if internal_temp is None or external_temp is None:
_LOGGER.warning("Internal_temp or external_temp are not set. Regulation will be suspended")
return self.target_temp
# Calculate the error factor (P)
error = self.target_temp - internal_temp

View File

@@ -3,14 +3,29 @@
import logging
from datetime import timedelta
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
from homeassistant.components.climate import HVACAction, HVACMode
from .base_thermostat import BaseThermostat
from .pi_algorithm import PITemperatureRegulator
from .const import CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4, overrides
from .const import (
overrides,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
RegulationParamLight,
RegulationParamMedium,
RegulationParamStrong
)
from .underlyings import UnderlyingClimate
@@ -18,6 +33,9 @@ _LOGGER = logging.getLogger(__name__)
class ThermostatOverClimate(BaseThermostat):
"""Representation of a base class for a Versatile Thermostat over a climate"""
_regulation_mode:str = None
_regulation_algo = None
_regulated_target_temp: float = None
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
{
@@ -25,10 +43,11 @@ class ThermostatOverClimate(BaseThermostat):
"underlying_climate_2", "underlying_climate_3"
}))
# Useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat over switch."""
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
super().__init__(hass, unique_id, name, entry_infos)
self._regulated_target_temp = self.target_temperature
@property
def is_over_climate(self) -> bool:
@@ -60,10 +79,22 @@ class ThermostatOverClimate(BaseThermostat):
"""Set the target temperature and the target temperature of underlying climate if any"""
await super()._async_internal_set_temperature(temperature)
for under in self._underlyings:
await under.set_temperature(
temperature, self._attr_max_temp, self._attr_min_temp
)
self._regulation_algo.set_target_temp(self.target_temperature)
await self._send_regulated_temperature()
async def _send_regulated_temperature(self):
""" Sends the regulated temperature to all underlying """
new_regulated_temp = self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp)
if new_regulated_temp != self._regulated_target_temp:
_LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp)
self._regulated_target_temp = new_regulated_temp
for under in self._underlyings:
await under.set_temperature(
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
)
else:
_LOGGER.debug("%s - No change on regulated temperature (%.1f)", self, self._regulated_target_temp)
@overrides
def post_init(self, entry_infos):
@@ -85,7 +116,39 @@ class ThermostatOverClimate(BaseThermostat):
)
)
self._regulation_mode = entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE
if self._regulation_mode == CONF_AUTO_REGULATION_LIGHT:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamLight.kp,
RegulationParamLight.ki,
RegulationParamLight.k_ext,
RegulationParamLight.offset_max,
RegulationParamLight.stabilization_threshold,
RegulationParamLight.accumulated_error_threshold)
elif self._regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamMedium.kp,
RegulationParamMedium.ki,
RegulationParamMedium.k_ext,
RegulationParamMedium.offset_max,
RegulationParamMedium.stabilization_threshold,
RegulationParamMedium.accumulated_error_threshold)
elif self._regulation_mode == CONF_AUTO_REGULATION_STRONG:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamStrong.kp,
RegulationParamStrong.ki,
RegulationParamStrong.k_ext,
RegulationParamStrong.offset_max,
RegulationParamStrong.stabilization_threshold,
RegulationParamStrong.accumulated_error_threshold)
else:
# A default empty algo (which does nothing)
self._regulation_algo = PITemperatureRegulator(
self.target_temperature, 0, 0, 0, 0, 0.1, 0)
@overrides
async def async_added_to_hass(self):
@@ -132,6 +195,9 @@ class ThermostatOverClimate(BaseThermostat):
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
if self.is_regulated:
self._attr_extra_state_attributes["regulated_target_temp"] = self._regulated_target_temp
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
@@ -352,6 +418,30 @@ class ThermostatOverClimate(BaseThermostat):
await end_climate_changed(changes)
@overrides
async def async_control_heating(self, force=False, _=None):
"""The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _)
await self._send_regulated_temperature()
return ret
@property
def regulation_mode(self):
""" Get the regulation mode """
return self._regulation_mode
@property
def regulated_target_temp(self):
""" Get the regulated target temperature """
return self._regulated_target_temp
@property
def is_regulated(self):
""" Check if the ThermostatOverClimate is regulated """
return self.regulation_mode != CONF_AUTO_REGULATION_NONE
@property
def hvac_modes(self):
"""List of available operation modes."""

View File

@@ -50,6 +50,8 @@ from custom_components.versatile_thermostat.const import (
CONF_PRESENCE_SENSOR,
PRESET_AWAY_SUFFIX,
CONF_CLIMATE,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_MEDIUM
)
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName",
@@ -89,14 +91,14 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_DEVICE_POWER: 1
# Keep default values which are False
}
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_AC_MODE: False
}
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
@@ -122,6 +124,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM
}
MOCK_PRESETS_CONFIG = {

View File

@@ -43,7 +43,7 @@ def test_pi_algorithm_basics():
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
def test_pi_algorithm_very_light():
def test_pi_algorithm_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)