""" The TPI calculation module """ # pylint: disable='line-too-long' import logging from vtherm_api.log_collector import get_vtherm_logger from .vtherm_hvac_mode import VThermHvacMode, VThermHvacMode_OFF, VThermHvacMode_COOL, VThermHvacMode_SLEEP _LOGGER = get_vtherm_logger(__name__) def is_number(value): """check if value is a number""" return isinstance(value, (int, float)) class TpiAlgorithm: """This class aims to do all calculation of the Proportional alogorithm""" def __init__( self, tpi_coef_int, tpi_coef_ext, vtherm_entity_id: str = None, max_on_percent: float = None, tpi_threshold_low: float = 0.0, tpi_threshold_high: float = 0.0, ) -> None: """Initialisation of the Proportional Algorithm""" _LOGGER.debug( "%s - Creation new TpiAlgorithm tpi_coef_int: %s, tpi_coef_ext: %s, tpi_threshold_low=%s, tpi_threshold_high=%s", # pylint: disable=line-too-long vtherm_entity_id, tpi_coef_int, tpi_coef_ext, tpi_threshold_low, tpi_threshold_high, ) # Issue 506 - check parameters if ( vtherm_entity_id is None or not is_number(tpi_coef_int) or not is_number(tpi_coef_ext) ): _LOGGER.error( "%s - configuration is wrong. entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s", vtherm_entity_id, vtherm_entity_id, tpi_coef_int, tpi_coef_ext, ) raise TypeError( "TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values" ) self._vtherm_entity_id = vtherm_entity_id self._tpi_coef_int = tpi_coef_int self._tpi_coef_ext = tpi_coef_ext self._on_percent = 0 self._calculated_on_percent = 0 self._total_on_percent = 0 self._max_on_percent = max_on_percent self._tpi_threshold_low = tpi_threshold_low self._tpi_threshold_high = tpi_threshold_high self._apply_threshold = tpi_threshold_low != 0.0 and tpi_threshold_high != 0.0 # True once calculate() has been called with a valid temperature. # When False, on_percent returns None so the cycle scheduler # does not touch an already-active switch at startup. self._temperature_available: bool = False def calculate( self, target_temp: float | None, current_temp: float | None, ext_current_temp: float | None, slope: float | None, hvac_mode: VThermHvacMode, **kwargs, ): """Do the calculation of the duration""" if target_temp is None or current_temp is None: log = _LOGGER.debug if hvac_mode == VThermHvacMode_OFF else _LOGGER.warning log( "%s - Proportional algorithm: calculation is not possible cause target_temp (%s) or current_temp (%s) is null. Heating/cooling will be disabled. This could be normal at startup", # pylint: disable=line-too-long self._vtherm_entity_id, target_temp, current_temp, ) self._calculated_on_percent = 0 self._temperature_available = False else: self._temperature_available = True if hvac_mode == VThermHvacMode_COOL: delta_temp = current_temp - target_temp delta_ext_temp = ext_current_temp - target_temp if ext_current_temp is not None else 0 slope = -slope if slope is not None else None else: delta_temp = target_temp - current_temp delta_ext_temp = target_temp - ext_current_temp if ext_current_temp is not None else 0 # Apply thresholds if ( # fmt: off self._apply_threshold and slope is not None and ((slope > 0.0 and -delta_temp > self._tpi_threshold_high) or (slope < 0.0 and -delta_temp > self._tpi_threshold_low)) # fmt: on ): _LOGGER.debug( "%s - Proportional algorithm: on_percent is forced to 0 cause current_temp (%.1f) is outside the thresholds (slope=%.1f, target_temp=%.1f, tpi_threshold_low=%.1f, tpi_threshold_high=%.1f). Heating/cooling will be disabled.", # pylint: disable=line-too-long self._vtherm_entity_id, current_temp, slope, target_temp, self._tpi_threshold_low, self._tpi_threshold_high, ) self._calculated_on_percent = 0 else: if hvac_mode not in [VThermHvacMode_OFF, VThermHvacMode_SLEEP]: self._calculated_on_percent = self._tpi_coef_int * delta_temp + self._tpi_coef_ext * delta_ext_temp # calculated on_time duration in seconds if self._calculated_on_percent > 1: self._calculated_on_percent = 1 if self._calculated_on_percent < 0: self._calculated_on_percent = 0 if self._max_on_percent is not None and self._calculated_on_percent > self._max_on_percent: self._calculated_on_percent = self._max_on_percent else: _LOGGER.debug( "%s - Proportional algorithm: VTherm is off. Heating will be disabled", self._vtherm_entity_id, ) self._calculated_on_percent = 0 _LOGGER.debug( "%s - heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f", # pylint: disable=line-too-long self._vtherm_entity_id, current_temp if current_temp else -9999.0, ext_current_temp if ext_current_temp else -9999.0, target_temp if target_temp else -9999.0, self._calculated_on_percent, ) def update_parameters( self, tpi_coef_int=None, tpi_coef_ext=None, tpi_threshold_low=None, tpi_threshold_high=None, ): """Update the parameters of the algorithm""" if tpi_coef_int is not None: self._tpi_coef_int = tpi_coef_int if tpi_coef_ext is not None: self._tpi_coef_ext = tpi_coef_ext if tpi_threshold_low is not None: self._tpi_threshold_low = tpi_threshold_low if tpi_threshold_high is not None: self._tpi_threshold_high = tpi_threshold_high self._apply_threshold = self._tpi_threshold_low != 0.0 and self._tpi_threshold_high != 0.0 _LOGGER.debug( "%s - Parameters updated. tpi_coef_int: %s, tpi_coef_ext: %s, tpi_threshold_low: %s, tpi_threshold_high: %s", self._vtherm_entity_id, self._tpi_coef_int, self._tpi_coef_ext, self._tpi_threshold_low, self._tpi_threshold_high, ) def update_realized_power(self, power_percent: float): """Update the realized power_percent. This method is called by the VTherm when the power is modified by safety or other features which clamps the value or force it to 0 or 1. """ self._total_on_percent = power_percent if self._total_on_percent != self._calculated_on_percent: _LOGGER.debug( "%s - Realized power is different from calculated power. Calculated: %.2f, Realized: %.2f", self._vtherm_entity_id, self._calculated_on_percent, self._total_on_percent, ) @property def on_percent(self) -> float | None: """Returns the percentage the heater must be ON. Returns None when no valid temperature was available at the last calculate() call. In that case callers should preserve the current switch state rather than forcing it off. Note: This does NOT reflect safety overrides. Use calculated_on_percent for display or check ThermostatProp.on_percent for the effective value. """ if not self._temperature_available: return None return round(self._calculated_on_percent, 2) @property def calculated_on_percent(self) -> float: """Returns the calculated percentage the heater must be ON Calculated means NOT overriden even in safety mode (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long return round(self._calculated_on_percent, 2) @property def tpi_coef_int(self) -> float: """Returns the TPI coefficient int""" return self._tpi_coef_int @property def tpi_coef_ext(self) -> float: """Returns the TPI coefficient ext""" return self._tpi_coef_ext @property def tpi_threshold_low(self) -> float: """Returns the TPI threshold low""" return self._tpi_threshold_low @property def tpi_threshold_high(self) -> float: """Returns the TPI threshold high""" return self._tpi_threshold_high