228 lines
9.2 KiB
Python
228 lines
9.2 KiB
Python
""" 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
|