Added VT Thermometer to HACS
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
# pylint: disable=line-too-long, abstract-method
|
||||
"""TPI algorithm handler for ThermostatProp."""
|
||||
|
||||
import logging
|
||||
from vtherm_api.log_collector import get_vtherm_logger
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
|
||||
from .prop_algo_tpi import TpiAlgorithm
|
||||
from .auto_tpi_manager import AutoTpiManager
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .vtherm_hvac_mode import VThermHvacMode_OFF, VThermHvacMode_HEAT, VThermHvacMode_COOL
|
||||
from .vtherm_central_api import VersatileThermostatAPI
|
||||
from .commons import write_event_log
|
||||
from .cycle_scheduler import calculate_cycle_times
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .thermostat_prop import ThermostatProp
|
||||
|
||||
_LOGGER = get_vtherm_logger(__name__)
|
||||
|
||||
|
||||
class TPIHandler:
|
||||
"""Handler for TPI-specific logic.
|
||||
|
||||
This class encapsulates all TPI-specific behavior and is used
|
||||
via composition by ThermostatProp. It updates thermostat attributes
|
||||
directly for backward compatibility with child classes.
|
||||
"""
|
||||
|
||||
def __init__(self, thermostat: "ThermostatProp"):
|
||||
"""Initialize handler with parent thermostat reference."""
|
||||
self._thermostat = thermostat
|
||||
self._auto_tpi_manager: AutoTpiManager | None = None
|
||||
# Save default values for Auto TPI reset
|
||||
self._default_coef_int: float = 0
|
||||
self._default_coef_ext: float = 0
|
||||
|
||||
@property
|
||||
def tpi_coef_int(self) -> float:
|
||||
"""Return TPI internal coefficient from thermostat."""
|
||||
return self._thermostat.tpi_coef_int
|
||||
|
||||
@property
|
||||
def tpi_coef_ext(self) -> float:
|
||||
"""Return TPI external coefficient from thermostat."""
|
||||
return self._thermostat.tpi_coef_ext
|
||||
|
||||
@property
|
||||
def tpi_threshold_low(self) -> float:
|
||||
"""Return TPI low threshold from thermostat."""
|
||||
return self._thermostat.tpi_threshold_low
|
||||
|
||||
@property
|
||||
def tpi_threshold_high(self) -> float:
|
||||
"""Return TPI high threshold from thermostat."""
|
||||
return self._thermostat.tpi_threshold_high
|
||||
|
||||
@property
|
||||
def minimal_activation_delay(self) -> int:
|
||||
"""Return minimal activation delay from thermostat."""
|
||||
return self._thermostat.minimal_activation_delay
|
||||
|
||||
@property
|
||||
def minimal_deactivation_delay(self) -> int:
|
||||
"""Return minimal deactivation delay from thermostat."""
|
||||
return self._thermostat.minimal_deactivation_delay
|
||||
|
||||
@property
|
||||
def proportional_function(self) -> str | None:
|
||||
"""Return proportional function from thermostat."""
|
||||
return self._thermostat.proportional_function
|
||||
|
||||
@property
|
||||
def auto_tpi_manager(self) -> AutoTpiManager | None:
|
||||
"""Return the Auto TPI manager."""
|
||||
return self._auto_tpi_manager
|
||||
|
||||
def init_algorithm(self):
|
||||
"""Initialize PropAlgorithm and AutoTpiManager.
|
||||
|
||||
Updates thermostat attributes directly for backward compatibility.
|
||||
"""
|
||||
t = self._thermostat
|
||||
entry = t.entry_infos
|
||||
|
||||
# Read and set TPI-specific config on thermostat using public setters
|
||||
t.tpi_coef_int = entry.get(CONF_TPI_COEF_INT)
|
||||
t.tpi_coef_ext = entry.get(CONF_TPI_COEF_EXT)
|
||||
t.tpi_threshold_low = entry.get(CONF_TPI_THRESHOLD_LOW, 0.0)
|
||||
t.tpi_threshold_high = entry.get(CONF_TPI_THRESHOLD_HIGH, 0.0)
|
||||
t.minimal_activation_delay = entry.get(CONF_MINIMAL_ACTIVATION_DELAY, 0)
|
||||
t.minimal_deactivation_delay = entry.get(CONF_MINIMAL_DEACTIVATION_DELAY, 0)
|
||||
|
||||
# Save default values for Auto TPI reset
|
||||
self._default_coef_int = t.tpi_coef_int
|
||||
self._default_coef_ext = t.tpi_coef_ext
|
||||
|
||||
# Validation: thresholds - if one is 0 then both are 0
|
||||
if t.tpi_threshold_low == 0.0 or t.tpi_threshold_high == 0.0:
|
||||
t.tpi_threshold_low = 0.0
|
||||
t.tpi_threshold_high = 0.0
|
||||
|
||||
# Validation: delays
|
||||
if (t.minimal_activation_delay + t.minimal_deactivation_delay) / 60 > t.cycle_min:
|
||||
_LOGGER.warning(
|
||||
"%s - The sum of minimal_activation_delay (%s sec) and "
|
||||
"minimal_deactivation_delay (%s sec) is greater than cycle_min (%s). "
|
||||
"This can create some unexpected behavior. Please review your configuration",
|
||||
t,
|
||||
t.minimal_activation_delay,
|
||||
t.minimal_deactivation_delay,
|
||||
t.cycle_min,
|
||||
)
|
||||
|
||||
# Validation: external temp sensor for TPI
|
||||
if (
|
||||
t.proportional_function == PROPORTIONAL_FUNCTION_TPI
|
||||
and t.ext_temp_sensor_entity_id is None
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Using TPI function but not external temperature sensor is set. "
|
||||
"Removing the delta temp ext factor. "
|
||||
"Thermostat will not be fully operational."
|
||||
)
|
||||
t.tpi_coef_ext = 0
|
||||
|
||||
# Create TpiAlgorithm on thermostat
|
||||
t.prop_algorithm = TpiAlgorithm(
|
||||
t.tpi_coef_int,
|
||||
t.tpi_coef_ext,
|
||||
t.name,
|
||||
max_on_percent=t.max_on_percent,
|
||||
tpi_threshold_low=t.tpi_threshold_low,
|
||||
tpi_threshold_high=t.tpi_threshold_high,
|
||||
)
|
||||
|
||||
# Initialize Auto TPI Manager from config
|
||||
heater_heating_time = entry.get(CONF_AUTO_TPI_HEATER_HEATING_TIME, 5)
|
||||
heater_cooling_time = entry.get(CONF_AUTO_TPI_HEATER_COOLING_TIME, 5)
|
||||
calculation_method = entry.get(CONF_AUTO_TPI_CALCULATION_METHOD, AUTO_TPI_METHOD_EMA)
|
||||
ema_alpha = entry.get(CONF_AUTO_TPI_EMA_ALPHA, 0.2)
|
||||
avg_initial_weight = entry.get(CONF_AUTO_TPI_AVG_INITIAL_WEIGHT, 1)
|
||||
|
||||
heating_rate = entry.get(CONF_AUTO_TPI_HEATING_POWER, 1.0)
|
||||
cooling_rate = entry.get(CONF_AUTO_TPI_COOLING_POWER, 1.0)
|
||||
aggressiveness = entry.get(CONF_AUTO_TPI_AGGRESSIVENESS, 1.0)
|
||||
continuous_kext = entry.get(CONF_AUTO_TPI_CONTINUOUS_KEXT, False)
|
||||
continuous_kext_alpha = entry.get(CONF_AUTO_TPI_CONTINUOUS_KEXT_ALPHA, 0.04)
|
||||
|
||||
_LOGGER.info("%s - DEBUG: TPI coefficients from entry_infos: int=%.3f, ext=%.3f",
|
||||
t, t.tpi_coef_int, t.tpi_coef_ext)
|
||||
|
||||
self._auto_tpi_manager = AutoTpiManager(
|
||||
t.hass,
|
||||
t.config_entry,
|
||||
t.unique_id,
|
||||
t.name,
|
||||
t.cycle_min,
|
||||
t.tpi_threshold_low,
|
||||
t.tpi_threshold_high,
|
||||
t.minimal_deactivation_delay,
|
||||
coef_int=t.tpi_coef_int,
|
||||
coef_ext=t.tpi_coef_ext,
|
||||
heater_heating_time=heater_heating_time,
|
||||
heater_cooling_time=heater_cooling_time,
|
||||
calculation_method=calculation_method,
|
||||
ema_alpha=ema_alpha,
|
||||
avg_initial_weight=avg_initial_weight,
|
||||
heating_rate=heating_rate,
|
||||
cooling_rate=cooling_rate,
|
||||
aggressiveness=aggressiveness,
|
||||
continuous_kext=continuous_kext,
|
||||
continuous_kext_alpha=continuous_kext_alpha,
|
||||
)
|
||||
self._auto_tpi_manager.set_is_vtherm_stopping_callback(lambda: t.is_removed)
|
||||
_LOGGER.info("%s - DEBUG: AutoTpiManager initialized with defaults: int=%.3f, ext=%.3f",
|
||||
t, self._auto_tpi_manager._default_coef_int, self._auto_tpi_manager._default_coef_ext)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Load Auto TPI data."""
|
||||
t = self._thermostat
|
||||
if self._auto_tpi_manager:
|
||||
# Set entity_id for pre-bootstrap calibration sensor lookup
|
||||
self._auto_tpi_manager._entity_id = t.entity_id
|
||||
_LOGGER.info("%s - DEBUG: Before load_data - int=%.3f, ext=%.3f", t, t.tpi_coef_int, t.tpi_coef_ext)
|
||||
await self._auto_tpi_manager.async_load_data()
|
||||
|
||||
# If we have learned parameters, apply them
|
||||
learned_params = self._auto_tpi_manager.get_calculated_params()
|
||||
if learned_params:
|
||||
_LOGGER.info("%s - DEBUG: Learned params found: %s, learning_active=%s",
|
||||
t, learned_params, self._auto_tpi_manager.learning_active)
|
||||
if self._auto_tpi_manager.learning_active:
|
||||
t.tpi_coef_int = learned_params.get(CONF_TPI_COEF_INT, t.tpi_coef_int)
|
||||
t.tpi_coef_ext = learned_params.get(CONF_TPI_COEF_EXT, t.tpi_coef_ext)
|
||||
_LOGGER.info("%s - Restored Auto TPI parameters: %s", t, learned_params)
|
||||
else:
|
||||
_LOGGER.info("%s - Auto TPI parameters found but not applied because learning is disabled", t)
|
||||
|
||||
_LOGGER.info("%s - DEBUG: After load_data - int=%.3f, ext=%.3f",
|
||||
t, t.tpi_coef_int, t.tpi_coef_ext)
|
||||
|
||||
if self._auto_tpi_manager.learning_active:
|
||||
# Security: if the feature is disabled in config, we must stop learning
|
||||
if not t.entry_infos.get(CONF_AUTO_TPI_MODE, False):
|
||||
_LOGGER.info("%s - Auto TPI learning was active but feature is disabled in config. Stopping learning.", t)
|
||||
await self._auto_tpi_manager.stop_learning()
|
||||
else:
|
||||
_LOGGER.info("%s - Auto TPI learning is active (restored from storage)", t)
|
||||
|
||||
async def async_startup(self):
|
||||
"""Startup actions."""
|
||||
# No more active cycle loop to start, it's now handled passively in control_heating
|
||||
pass
|
||||
|
||||
def remove(self):
|
||||
"""Cleanup on removal."""
|
||||
t = self._thermostat
|
||||
if self._auto_tpi_manager:
|
||||
t.hass.async_create_task(self._auto_tpi_manager.async_save_data())
|
||||
|
||||
def on_scheduler_ready(self, scheduler) -> None:
|
||||
"""Register AutoTPI learning callbacks on the cycle scheduler."""
|
||||
if self._auto_tpi_manager:
|
||||
scheduler.register_cycle_start_callback(self._auto_tpi_manager.on_cycle_started)
|
||||
scheduler.register_cycle_end_callback(self._auto_tpi_manager.on_cycle_completed)
|
||||
|
||||
def should_publish_intermediate(self) -> bool:
|
||||
"""TPI always publishes the current control iteration."""
|
||||
return True
|
||||
|
||||
def _is_central_boiler_off(self) -> bool:
|
||||
"""Check if the central boiler is configured but currently off."""
|
||||
t = self._thermostat
|
||||
if not t.is_used_by_central_boiler:
|
||||
return False
|
||||
api = VersatileThermostatAPI.get_vtherm_api()
|
||||
if api and api.central_boiler_manager:
|
||||
return not api.central_boiler_manager.is_on
|
||||
return False
|
||||
|
||||
async def _get_tpi_data(self) -> dict[str, Any]:
|
||||
"""Calculate and return TPI cycle parameters."""
|
||||
t = self._thermostat
|
||||
|
||||
# Feed current temperatures to AutoTpiManager BEFORE getting params
|
||||
if self._auto_tpi_manager:
|
||||
await self._auto_tpi_manager.update(
|
||||
room_temp=t.current_temperature,
|
||||
ext_temp=t.current_outdoor_temperature,
|
||||
target_temp=t.target_temperature,
|
||||
hvac_mode=str(t.vtherm_hvac_mode),
|
||||
is_overpowering_detected=t.power_manager.is_overpowering_detected,
|
||||
is_central_boiler_off=self._is_central_boiler_off(),
|
||||
is_heating_failure=t.heating_failure_detection_manager.is_failure_detected,
|
||||
)
|
||||
|
||||
# Sync coefficients from AutoTpiManager before calculating
|
||||
if self._auto_tpi_manager and self._auto_tpi_manager.learning_active:
|
||||
new_params = await self._auto_tpi_manager.calculate()
|
||||
if new_params:
|
||||
new_coef_int = new_params.get(CONF_TPI_COEF_INT)
|
||||
new_coef_ext = new_params.get(CONF_TPI_COEF_EXT)
|
||||
if new_coef_int != t.prop_algorithm.tpi_coef_int or \
|
||||
new_coef_ext != t.prop_algorithm.tpi_coef_ext:
|
||||
t.prop_algorithm.update_parameters(tpi_coef_int=new_coef_int, tpi_coef_ext=new_coef_ext)
|
||||
_LOGGER.debug("%s - Synced TPI coeffs before cycle: int=%.3f, ext=%.3f",
|
||||
t, new_coef_int, new_coef_ext)
|
||||
|
||||
# Force recalculation with potentially updated coefficients
|
||||
t.recalculate()
|
||||
|
||||
# Calculate time (in seconds)
|
||||
if t.prop_algorithm:
|
||||
on_time_sec, off_time_sec, forced_by_timing = calculate_cycle_times(
|
||||
t.on_percent,
|
||||
t.cycle_min,
|
||||
t.minimal_activation_delay,
|
||||
t.minimal_deactivation_delay,
|
||||
)
|
||||
|
||||
realized_percent = on_time_sec / (t.cycle_min * 60)
|
||||
|
||||
# Notify prop_algorithm if forced by timing (existing behavior)
|
||||
if forced_by_timing:
|
||||
if t.prop_algorithm and hasattr(t.prop_algorithm, "update_realized_power"):
|
||||
t.prop_algorithm.update_realized_power(realized_percent)
|
||||
else:
|
||||
on_time_sec = 0
|
||||
off_time_sec = 0
|
||||
|
||||
return {
|
||||
"on_time_sec": on_time_sec,
|
||||
"off_time_sec": off_time_sec,
|
||||
"on_percent": t.safe_on_percent,
|
||||
"hvac_mode": str(t.vtherm_hvac_mode),
|
||||
}
|
||||
|
||||
async def control_heating(self, timestamp=None, force=False):
|
||||
"""TPI-specific control heating logic."""
|
||||
del timestamp
|
||||
t = self._thermostat
|
||||
|
||||
# Feed the Auto TPI manager
|
||||
if self._auto_tpi_manager:
|
||||
# 1. Update manager's transient state
|
||||
await self._auto_tpi_manager.update(
|
||||
room_temp=t.current_temperature,
|
||||
ext_temp=t.current_outdoor_temperature,
|
||||
target_temp=t.target_temperature,
|
||||
hvac_mode=str(t.vtherm_hvac_mode),
|
||||
is_overpowering_detected=t.power_manager.is_overpowering_detected,
|
||||
is_central_boiler_off=self._is_central_boiler_off(),
|
||||
is_heating_failure=t.heating_failure_detection_manager.is_failure_detected,
|
||||
)
|
||||
|
||||
# 2. Synchronize parameters if learning is active
|
||||
new_params = await self._auto_tpi_manager.calculate()
|
||||
if self._auto_tpi_manager.learning_active and new_params:
|
||||
new_coef_int = new_params.get(CONF_TPI_COEF_INT)
|
||||
new_coef_ext = new_params.get(CONF_TPI_COEF_EXT)
|
||||
if new_coef_int is not None and new_coef_ext is not None:
|
||||
if t.prop_algorithm:
|
||||
# Update effective algo parameters
|
||||
t.prop_algorithm.update_parameters(tpi_coef_int=new_coef_int, tpi_coef_ext=new_coef_ext)
|
||||
# Keep thermostat attributes in sync
|
||||
t.tpi_coef_int = new_coef_int
|
||||
t.tpi_coef_ext = new_coef_ext
|
||||
_LOGGER.debug("%s - Synced PropAlgorithm with current Auto TPI coeffs: int=%.3f, ext=%.3f",
|
||||
t, new_coef_int, new_coef_ext)
|
||||
|
||||
# Stop here if we are off
|
||||
if t.vtherm_hvac_mode == VThermHvacMode_OFF:
|
||||
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", t)
|
||||
t._on_time_sec = 0
|
||||
t._off_time_sec = int(t.cycle_min * 60)
|
||||
if t.is_device_active:
|
||||
await t.async_underlying_entity_turn_off()
|
||||
elif t.cycle_scheduler and t.cycle_scheduler.is_cycle_running:
|
||||
# The master scheduler may still hold a pending heat cycle while
|
||||
# the physical device is already in its OFF phase.
|
||||
await t.cycle_scheduler.cancel_cycle()
|
||||
else:
|
||||
on_percent = 0
|
||||
if t.prop_algorithm:
|
||||
on_percent = t.on_percent
|
||||
if on_percent is None:
|
||||
# Temperature sensor was not yet available at the last
|
||||
# recalculate() call (e.g. HA restart before sensor comes
|
||||
# back online). Preserve the current switch state instead of
|
||||
# turning it off with on_percent=0 (bug 1884).
|
||||
_LOGGER.info(
|
||||
"%s - on_percent is None (temperature unavailable). " "Skipping cycle to preserve current switch state.",
|
||||
t,
|
||||
)
|
||||
return
|
||||
|
||||
on_time_sec, off_time_sec, forced_by_timing = calculate_cycle_times(
|
||||
on_percent,
|
||||
t.cycle_min,
|
||||
t.minimal_activation_delay,
|
||||
t.minimal_deactivation_delay,
|
||||
)
|
||||
|
||||
realized_percent = on_time_sec / (t.cycle_min * 60)
|
||||
|
||||
if forced_by_timing:
|
||||
if t.prop_algorithm and hasattr(t.prop_algorithm, "update_realized_power"):
|
||||
t.prop_algorithm.update_realized_power(realized_percent)
|
||||
|
||||
await t.cycle_scheduler.start_cycle(
|
||||
t.vtherm_hvac_mode,
|
||||
on_percent,
|
||||
force,
|
||||
)
|
||||
|
||||
async def on_state_changed(self, changed: bool):
|
||||
"""Handle state changes."""
|
||||
del changed
|
||||
# Cycle management is now passive, no need to start/stop loop
|
||||
pass
|
||||
|
||||
def update_attributes(self):
|
||||
"""Add TPI-specific attributes to thermostat."""
|
||||
t = self._thermostat
|
||||
t._attr_extra_state_attributes["specific_states"].update({
|
||||
"auto_tpi_state": "on" if self._auto_tpi_manager and self._auto_tpi_manager.learning_active else "off",
|
||||
"auto_tpi_continuous_kext": "on" if self._auto_tpi_manager and self._auto_tpi_manager._continuous_kext else "off",
|
||||
"auto_tpi_learning": (
|
||||
self._auto_tpi_manager.get_filtered_state()
|
||||
if self._auto_tpi_manager and (self._auto_tpi_manager.learning_active or self._auto_tpi_manager._continuous_kext)
|
||||
else {}
|
||||
),
|
||||
})
|
||||
|
||||
t._attr_extra_state_attributes["configuration"].update({
|
||||
"minimal_activation_delay_sec": t.minimal_activation_delay,
|
||||
"minimal_deactivation_delay_sec": t.minimal_deactivation_delay,
|
||||
})
|
||||
|
||||
async def _async_update_tpi_config_entry(self):
|
||||
"""Update the config entry with current TPI parameters."""
|
||||
t = self._thermostat
|
||||
entry = t.hass.config_entries.async_get_entry(t.unique_id)
|
||||
if entry:
|
||||
new_data = entry.data.copy()
|
||||
new_data[CONF_TPI_COEF_INT] = t.tpi_coef_int
|
||||
new_data[CONF_TPI_COEF_EXT] = t.tpi_coef_ext
|
||||
new_data[CONF_TPI_THRESHOLD_LOW] = t.tpi_threshold_low
|
||||
new_data[CONF_TPI_THRESHOLD_HIGH] = t.tpi_threshold_high
|
||||
new_data[CONF_MINIMAL_ACTIVATION_DELAY] = t.minimal_activation_delay
|
||||
new_data[CONF_MINIMAL_DEACTIVATION_DELAY] = t.minimal_deactivation_delay
|
||||
|
||||
result = t.hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
_LOGGER.debug("%s - Config entry updated with new TPI params: %s", t, result)
|
||||
|
||||
async def service_set_tpi_parameters(
|
||||
self,
|
||||
tpi_coef_int: float | None = None,
|
||||
tpi_coef_ext: float | None = None,
|
||||
minimal_activation_delay: int | None = None,
|
||||
minimal_deactivation_delay: int | None = None,
|
||||
tpi_threshold_low: float | None = None,
|
||||
tpi_threshold_high: float | None = None,
|
||||
):
|
||||
"""Service handler for set_tpi_parameters."""
|
||||
t = self._thermostat
|
||||
|
||||
if t.lock_manager.check_is_locked("service_set_tpi_parameters"):
|
||||
return
|
||||
|
||||
write_event_log(
|
||||
_LOGGER,
|
||||
t,
|
||||
f"Calling SERVICE_SET_TPI_PARAMETERS, tpi_coef_int: {tpi_coef_int}, "
|
||||
f"tpi_coef_ext: {tpi_coef_ext}"
|
||||
f"minimal_activation_delay: {minimal_activation_delay}, "
|
||||
f"minimal_deactivation_delay: {minimal_deactivation_delay}, "
|
||||
f"tpi_threshold_low: {tpi_threshold_low}, "
|
||||
f"tpi_threshold_high: {tpi_threshold_high}",
|
||||
)
|
||||
|
||||
if t.prop_algorithm is None:
|
||||
raise ServiceValidationError(f"{t} - No TPI algorithm configured for this thermostat.")
|
||||
|
||||
entry = t.hass.config_entries.async_get_entry(t.unique_id)
|
||||
if not entry:
|
||||
raise ServiceValidationError(f"{t} - No config entry has been found for this thermostat.")
|
||||
|
||||
if entry.data.get(CONF_USE_TPI_CENTRAL_CONFIG, False):
|
||||
raise ServiceValidationError(f"{t} - Impossible to set TPI parameters when using central TPI configuration.")
|
||||
|
||||
# Update the algorithm with coefficients and thresholds
|
||||
t.prop_algorithm.update_parameters(
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
tpi_threshold_low,
|
||||
tpi_threshold_high,
|
||||
)
|
||||
|
||||
# Update thermostat attributes directly (handler updates them via setters)
|
||||
t.tpi_coef_int = t.prop_algorithm.tpi_coef_int
|
||||
t.tpi_coef_ext = t.prop_algorithm.tpi_coef_ext
|
||||
t.tpi_threshold_low = t.prop_algorithm.tpi_threshold_low
|
||||
t.tpi_threshold_high = t.prop_algorithm.tpi_threshold_high
|
||||
|
||||
# Update delays directly on thermostat (not in algo anymore)
|
||||
if minimal_activation_delay is not None:
|
||||
t.minimal_activation_delay = minimal_activation_delay
|
||||
t.cycle_scheduler.min_activation_delay = t.minimal_activation_delay
|
||||
if minimal_deactivation_delay is not None:
|
||||
t.minimal_deactivation_delay = minimal_deactivation_delay
|
||||
t.cycle_scheduler.min_deactivation_delay = t.minimal_deactivation_delay
|
||||
|
||||
await self._async_update_tpi_config_entry()
|
||||
|
||||
if t.is_removed:
|
||||
_LOGGER.debug("%s - Entity is removed, stop service_set_tpi_parameters", t)
|
||||
return
|
||||
|
||||
t.recalculate()
|
||||
await t.async_control_heating(force=True)
|
||||
|
||||
async def service_set_auto_tpi_mode(
|
||||
self,
|
||||
auto_tpi_mode: bool,
|
||||
reinitialise: bool = True,
|
||||
allow_kint_boost_on_stagnation: bool = False,
|
||||
allow_kext_compensation_on_overshoot: bool = False,
|
||||
):
|
||||
"""Service handler for set_auto_tpi_mode."""
|
||||
t = self._thermostat
|
||||
|
||||
if t.proportional_function != PROPORTIONAL_FUNCTION_TPI:
|
||||
raise ServiceValidationError(f"{t} - This service is only available for TPI algorithm.")
|
||||
if not t.entry_infos.get(CONF_AUTO_TPI_MODE, False):
|
||||
raise ServiceValidationError(f"{t} - Auto TPI is not enabled in configuration.")
|
||||
|
||||
write_event_log(
|
||||
_LOGGER,
|
||||
t,
|
||||
f"Calling SERVICE_SET_AUTO_TPI_MODE, auto_tpi_mode: {auto_tpi_mode}, "
|
||||
f"reinitialise: {reinitialise}, "
|
||||
f"allow_kint_boost: {allow_kint_boost_on_stagnation}, "
|
||||
f"allow_kext_overshoot: {allow_kext_compensation_on_overshoot}",
|
||||
)
|
||||
await self.async_set_auto_tpi_mode(
|
||||
auto_tpi_mode,
|
||||
reinitialise,
|
||||
allow_kint_boost_on_stagnation,
|
||||
allow_kext_compensation_on_overshoot,
|
||||
)
|
||||
|
||||
async def service_auto_tpi_calibrate_capacity(
|
||||
self,
|
||||
save_to_config: bool,
|
||||
min_power_threshold: int,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
):
|
||||
"""Service handler for auto_tpi_calibrate_capacity."""
|
||||
t = self._thermostat
|
||||
|
||||
if t.proportional_function != PROPORTIONAL_FUNCTION_TPI:
|
||||
raise ServiceValidationError(f"{t} - This service is only available for TPI algorithm.")
|
||||
if not t.entry_infos.get(CONF_AUTO_TPI_MODE, False):
|
||||
raise ServiceValidationError(f"{t} - Auto TPI is not enabled in configuration.")
|
||||
|
||||
write_event_log(_LOGGER, t, f"Calling SERVICE_AUTO_TPI_CALIBRATE_CAPACITY, save_to_config: {save_to_config}, start_date: {start_date}, end_date: {end_date}, min_power_threshold: {min_power_threshold}")
|
||||
|
||||
if not self._auto_tpi_manager:
|
||||
raise ServiceValidationError(f"{t} - Auto TPI Manager not initialized, cannot calibrate capacity.")
|
||||
|
||||
result = await self._auto_tpi_manager.service_calibrate_capacity(
|
||||
thermostat_entity_id=t.entity_id,
|
||||
ext_temp_entity_id=t.ext_temp_sensor_entity_id,
|
||||
save_to_config=save_to_config,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
min_power_threshold=min_power_threshold / 100.0,
|
||||
)
|
||||
|
||||
if result and result.get("success") and result.get("max_capacity"):
|
||||
t.recalculate()
|
||||
|
||||
t.update_custom_attributes()
|
||||
t.async_write_ha_state()
|
||||
|
||||
return result
|
||||
|
||||
async def async_set_auto_tpi_mode(
|
||||
self,
|
||||
auto_tpi_mode: bool,
|
||||
reinitialise: bool = True,
|
||||
allow_kint_boost: bool = False,
|
||||
allow_kext_overshoot: bool = False,
|
||||
):
|
||||
"""Set the auto TPI mode."""
|
||||
t = self._thermostat
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - async_set_auto_tpi_mode called with auto_tpi_mode=%s, reinitialise=%s, kint_boost=%s, kext_overshoot=%s",
|
||||
t,
|
||||
auto_tpi_mode,
|
||||
reinitialise,
|
||||
allow_kint_boost,
|
||||
allow_kext_overshoot,
|
||||
)
|
||||
if not self._auto_tpi_manager:
|
||||
_LOGGER.warning("%s - Auto TPI Manager not initialized", t)
|
||||
return
|
||||
|
||||
# Safety check: Prevent enabling learning if the feature is disabled in config
|
||||
if auto_tpi_mode and not t.entry_infos.get(CONF_AUTO_TPI_MODE, False):
|
||||
_LOGGER.warning("%s - Cannot start Auto TPI Learning: feature is disabled in configuration", t)
|
||||
await self._auto_tpi_manager.stop_learning()
|
||||
return
|
||||
|
||||
if auto_tpi_mode:
|
||||
# Use the original configured default values
|
||||
await self._auto_tpi_manager.start_learning(
|
||||
coef_int=self._auto_tpi_manager._default_coef_int,
|
||||
coef_ext=self._auto_tpi_manager._default_coef_ext,
|
||||
reset_data=reinitialise,
|
||||
allow_kint_boost=allow_kint_boost,
|
||||
allow_kext_overshoot=allow_kext_overshoot,
|
||||
)
|
||||
|
||||
# Sync PropAlgorithm with the configured coefficients
|
||||
if t.prop_algorithm:
|
||||
t.prop_algorithm.update_parameters(tpi_coef_int=t.tpi_coef_int, tpi_coef_ext=t.tpi_coef_ext)
|
||||
_LOGGER.info("%s - PropAlgorithm synced with config: Kint=%.3f, Kext=%.3f",
|
||||
t, t.tpi_coef_int, t.tpi_coef_ext)
|
||||
|
||||
# If we enable auto_tpi, we must disable central config for TPI
|
||||
# Note: _entry_infos is a dict, we can update it directly
|
||||
if t._entry_infos:
|
||||
t._entry_infos[CONF_USE_TPI_CENTRAL_CONFIG] = False
|
||||
|
||||
# Persist the change to the config entry
|
||||
entry = t.hass.config_entries.async_get_entry(t.unique_id)
|
||||
if entry and entry.data.get(CONF_USE_TPI_CENTRAL_CONFIG, True):
|
||||
new_data = entry.data.copy()
|
||||
new_data[CONF_USE_TPI_CENTRAL_CONFIG] = False
|
||||
t.hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
if t.is_removed:
|
||||
_LOGGER.debug("%s - Entity is removed, stop async_set_auto_tpi_mode", t)
|
||||
return
|
||||
|
||||
# Starting learning is sufficient, cycle processing is passive
|
||||
pass
|
||||
else:
|
||||
await self._auto_tpi_manager.stop_learning()
|
||||
|
||||
# Apply configured coefficients to PropAlgorithm
|
||||
if t.prop_algorithm:
|
||||
t.prop_algorithm.update_parameters(tpi_coef_int=t.tpi_coef_int, tpi_coef_ext=t.tpi_coef_ext)
|
||||
_LOGGER.info(
|
||||
"%s - PropAlgorithm reset to config values: Kint=%.3f, Kext=%.3f",
|
||||
t, t.tpi_coef_int, t.tpi_coef_ext
|
||||
)
|
||||
|
||||
# Fire event to notify listeners
|
||||
t.hass.bus.async_fire(
|
||||
AUTO_TPI_EVENT,
|
||||
{
|
||||
"entity_id": t.entity_id,
|
||||
"auto_tpi_mode": self._auto_tpi_manager.learning_active,
|
||||
},
|
||||
)
|
||||
|
||||
# Force update of state attributes
|
||||
t.update_custom_attributes()
|
||||
t.async_write_ha_state()
|
||||
Reference in New Issue
Block a user