Implements regulation (tests ko)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user