319 lines
14 KiB
Python
319 lines
14 KiB
Python
""" Implements the Auto-start/stop Feature Manager """
|
|
|
|
# pylint: disable=line-too-long
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from vtherm_api.log_collector import get_vtherm_logger
|
|
from typing import Any
|
|
|
|
from homeassistant.core import (
|
|
HomeAssistant,
|
|
callback,
|
|
)
|
|
|
|
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
|
from .commons_type import ConfigData
|
|
|
|
from .commons import write_event_log
|
|
|
|
from .base_manager import BaseFeatureManager
|
|
|
|
from .auto_start_stop_algorithm import (
|
|
AutoStartStopDetectionAlgorithm,
|
|
)
|
|
|
|
|
|
_LOGGER = get_vtherm_logger(__name__)
|
|
|
|
|
|
class FeatureAutoStartStopManager(BaseFeatureManager):
|
|
"""The implementation of the AutoStartStop feature"""
|
|
|
|
unrecorded_attributes = frozenset(
|
|
{
|
|
"auto_start_stop_level",
|
|
"auto_start_stop_dtmin",
|
|
"auto_start_stop_enable",
|
|
"auto_start_stop_accumulated_error",
|
|
"auto_start_stop_accumulated_error_threshold",
|
|
"auto_start_stop_last_switch_date",
|
|
}
|
|
)
|
|
|
|
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
|
"""Init of a featureManager"""
|
|
super().__init__(vtherm, hass)
|
|
|
|
self._auto_start_stop_level: str = AUTO_START_STOP_LEVEL_NONE
|
|
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
|
self._is_configured: bool = False
|
|
self._is_auto_start_stop_enabled: bool = False
|
|
self._is_auto_stop_detected: bool = False
|
|
|
|
@overrides
|
|
def post_init(self, entry_infos: ConfigData):
|
|
"""Reinit of the manager"""
|
|
|
|
if not self._vtherm.is_over_climate or self._vtherm.have_valve_regulation:
|
|
return
|
|
|
|
use_auto_start_stop = entry_infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
|
|
if use_auto_start_stop:
|
|
self._auto_start_stop_level = (
|
|
entry_infos.get(CONF_AUTO_START_STOP_LEVEL, None)
|
|
or AUTO_START_STOP_LEVEL_NONE
|
|
)
|
|
self._is_configured = True
|
|
else:
|
|
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
|
|
self._is_configured = False
|
|
|
|
# Initialize the enable flag synchronously so the manager is operational
|
|
# without having to wait for the AutoStartStopEnable switch's
|
|
# async_added_to_hass callback (which is racy under test load).
|
|
# The switch's async_added_to_hass will later override this value
|
|
# from the persisted state if needed.
|
|
# This must mirror the default in switch.AutoStartStopEnable.
|
|
self._is_auto_start_stop_enabled = self._auto_start_stop_level != AUTO_START_STOP_LEVEL_NONE
|
|
|
|
# Instanciate the auto start stop algo
|
|
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
|
|
self._auto_start_stop_level, self.name
|
|
)
|
|
|
|
# Fix an eventual incoherent state
|
|
if self._vtherm.is_on and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP:
|
|
self._vtherm.hvac_off_reason = None
|
|
|
|
@overrides
|
|
async def start_listening(self):
|
|
"""Start listening the underlying entity"""
|
|
|
|
@overrides
|
|
def stop_listening(self):
|
|
"""Stop listening and remove the eventual timer still running"""
|
|
|
|
@overrides
|
|
async def refresh_state(self) -> bool:
|
|
"""Check the auto-start-stop and an eventual action
|
|
Return True is auto stop is detected"""
|
|
|
|
if not self._is_configured or not self._is_auto_start_stop_enabled:
|
|
_LOGGER.debug("%s - auto start/stop is disabled (or not configured)", self)
|
|
self._is_auto_stop_detected = False
|
|
else:
|
|
# Do the auto-start-stop calculation
|
|
slope = (self._vtherm.last_temperature_slope or 0) / 60 # to have the slope in °/min
|
|
should_be_off = self._auto_start_stop_algo.should_be_turned_off(
|
|
self._vtherm.requested_state.hvac_mode,
|
|
self._vtherm.target_temperature,
|
|
self._vtherm.current_temperature,
|
|
slope,
|
|
self._vtherm.now,
|
|
)
|
|
_LOGGER.debug("%s - auto_start_stop should be off is %s", self, should_be_off)
|
|
if should_be_off:
|
|
_LOGGER.info("%s - VTherm should be OFF due to auto-start-stop conditions", self)
|
|
# self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
|
|
# await self._vtherm.async_turn_off()
|
|
|
|
# Send an event if vtherm is on
|
|
if not self._is_auto_stop_detected:
|
|
self._vtherm.send_event(
|
|
event_type=EventType.AUTO_START_STOP_EVENT,
|
|
data={
|
|
"type": "stop",
|
|
"name": self.name,
|
|
"cause": "Auto stop conditions reached",
|
|
"hvac_mode": str(VThermHvacMode_OFF),
|
|
"saved_hvac_mode": str(self._vtherm.requested_state.hvac_mode),
|
|
"target_temperature": self._vtherm.target_temperature,
|
|
"current_temperature": self._vtherm.current_temperature,
|
|
"temperature_slope": round(slope, 3),
|
|
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
|
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
|
},
|
|
)
|
|
self._is_auto_stop_detected = True
|
|
|
|
else:
|
|
_LOGGER.info("%s - VTherm should be ON due to auto-start-stop conditions", self)
|
|
|
|
# await self._vtherm.async_turn_on()
|
|
|
|
# Send an event
|
|
if self._is_auto_stop_detected:
|
|
self._vtherm.send_event(
|
|
event_type=EventType.AUTO_START_STOP_EVENT,
|
|
data={
|
|
"type": "start",
|
|
"name": self.name,
|
|
"cause": "Auto start conditions reached",
|
|
"hvac_mode": str(self._vtherm.requested_state.hvac_mode),
|
|
"saved_hvac_mode": str(self._vtherm.requested_state.hvac_mode),
|
|
"target_temperature": self._vtherm.target_temperature,
|
|
"current_temperature": self._vtherm.current_temperature,
|
|
"temperature_slope": round(slope, 3),
|
|
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
|
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
|
},
|
|
)
|
|
self._is_auto_stop_detected = False
|
|
|
|
# returns True if we should stop
|
|
return self._is_auto_stop_detected
|
|
|
|
async def refresh_and_update_if_changed(self) -> bool:
|
|
"""Refresh the auto start/stop state and update_states of VTherm if changed
|
|
Returns True if the state has changed, False otherwise"""
|
|
old_auto_start_stop: bool = self.is_auto_stop_detected
|
|
if old_auto_start_stop != await self.refresh_state():
|
|
write_event_log(_LOGGER, self._vtherm, f"Auto start/stop state changed from {old_auto_start_stop} to {self.is_auto_stop_detected}")
|
|
self._vtherm.requested_state.force_changed()
|
|
await self._vtherm.update_states(force=True)
|
|
return True
|
|
|
|
return False
|
|
|
|
async def set_auto_start_stop_enable(self, is_enabled: bool):
|
|
"""Enable/Disable the auto-start/stop feature"""
|
|
if self._is_auto_start_stop_enabled != is_enabled:
|
|
self._is_auto_start_stop_enabled = is_enabled
|
|
|
|
# Send an event if the vtherm was off due to auto-start/stop and enable has been set to false
|
|
if not is_enabled and self._vtherm.hvac_mode == VThermHvacMode_OFF and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP:
|
|
_LOGGER.debug("%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm")
|
|
# Send an event
|
|
self._vtherm.send_event(
|
|
event_type=EventType.AUTO_START_STOP_EVENT,
|
|
data={
|
|
"type": "start",
|
|
"name": self.name,
|
|
"cause": "Auto start stop disabled",
|
|
"hvac_mode": str(self._vtherm.requested_state.hvac_mode),
|
|
"saved_hvac_mode": str(self._vtherm.requested_state.hvac_mode),
|
|
"target_temperature": self._vtherm.target_temperature,
|
|
"current_temperature": self._vtherm.current_temperature,
|
|
"temperature_slope": round(self._vtherm.last_temperature_slope or 0, 3),
|
|
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
|
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
|
},
|
|
)
|
|
|
|
await self.refresh_state()
|
|
self._vtherm.requested_state.force_changed()
|
|
await self._vtherm.update_states(True)
|
|
|
|
self._vtherm.update_custom_attributes()
|
|
|
|
@callback
|
|
@overrides
|
|
def restore_state(self, old_state) -> None:
|
|
"""Restore the auto start/stop manager state after a Home Assistant restart.
|
|
|
|
Restores:
|
|
- _is_auto_stop_detected: so the VTherm stays off if it was auto-stopped
|
|
- algo._accumulated_error: so the algorithm continues from where it left off
|
|
- algo._last_switch_date: to prevent rapid ON/OFF switching after restart
|
|
- algo._last_should_be_off: kept in sync with _is_auto_stop_detected
|
|
"""
|
|
if old_state is None:
|
|
return
|
|
|
|
manager_attr = old_state.attributes.get("auto_start_stop_manager")
|
|
if not manager_attr:
|
|
return
|
|
|
|
# Restore the manager-level detected state
|
|
self._is_auto_stop_detected = bool(manager_attr.get("is_auto_stop_detected", False))
|
|
|
|
# Restore algorithm intermediate state
|
|
if self._auto_start_stop_algo is not None:
|
|
accumulated_error = manager_attr.get("auto_start_stop_accumulated_error")
|
|
if accumulated_error is not None:
|
|
self._auto_start_stop_algo._accumulated_error = float(accumulated_error)
|
|
|
|
last_switch_date_raw = manager_attr.get("auto_start_stop_last_switch_date")
|
|
if last_switch_date_raw is not None:
|
|
if isinstance(last_switch_date_raw, str):
|
|
try:
|
|
self._auto_start_stop_algo._last_switch_date = datetime.fromisoformat(last_switch_date_raw)
|
|
except (ValueError, TypeError) as err:
|
|
_LOGGER.warning("%s - restore_state: could not parse last_switch_date '%s': %s", self, last_switch_date_raw, err)
|
|
else:
|
|
self._auto_start_stop_algo._last_switch_date = last_switch_date_raw
|
|
|
|
# Keep algo's internal flag in sync with the manager state
|
|
self._auto_start_stop_algo._last_should_be_off = self._is_auto_stop_detected
|
|
|
|
_LOGGER.info(
|
|
"%s - restore_state: is_auto_stop_detected=%s, accumulated_error=%s",
|
|
self,
|
|
self._is_auto_stop_detected,
|
|
manager_attr.get("auto_start_stop_accumulated_error"),
|
|
)
|
|
|
|
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
|
"""Add some custom attributes"""
|
|
extra_state_attributes.update(
|
|
{
|
|
"is_auto_start_stop_configured": self.is_configured,
|
|
}
|
|
)
|
|
if self.is_configured:
|
|
extra_state_attributes.update(
|
|
{
|
|
"auto_start_stop_manager": {
|
|
"auto_start_stop_enable": self.auto_start_stop_enable,
|
|
"auto_start_stop_level": self._auto_start_stop_algo.level,
|
|
"auto_start_stop_dtmin": self._auto_start_stop_algo.dt_min,
|
|
"auto_start_stop_accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
|
"auto_start_stop_accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
|
"auto_start_stop_last_switch_date": self._auto_start_stop_algo.last_switch_date,
|
|
"is_auto_stop_detected": self.is_auto_stop_detected,
|
|
}
|
|
}
|
|
)
|
|
|
|
@property
|
|
def is_auto_stop_detected(self) -> bool:
|
|
"""Return True if the auto-start/stop feature is detected"""
|
|
return self._is_auto_stop_detected
|
|
|
|
@property
|
|
def is_detected(self) -> bool:
|
|
"""Return True if the auto-start/stop feature is detected"""
|
|
return self._is_auto_stop_detected
|
|
|
|
@overrides
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
"""Return True of the aiuto-start/stop feature is configured"""
|
|
return self._is_configured
|
|
|
|
@property
|
|
def auto_start_stop_level(self) -> str:
|
|
"""Return the auto start/stop level."""
|
|
return self._auto_start_stop_level
|
|
|
|
@property
|
|
def auto_start_stop_enable(self) -> bool:
|
|
"""Returns the auto_start_stop_enable"""
|
|
return self._is_auto_start_stop_enabled
|
|
|
|
@property
|
|
def is_auto_stopped(self) -> bool:
|
|
"""Returns the is vtherm is stopped and reason is AUTO_START_STOP"""
|
|
return self._vtherm.hvac_mode == VThermHvacMode_OFF and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
|
|
|
def reset_switch_delay(self):
|
|
"""Reset the switch delay in the algorithm to allow immediate restart.
|
|
Should be called when target temperature changes significantly.
|
|
"""
|
|
if self._auto_start_stop_algo:
|
|
self._auto_start_stop_algo.reset_switch_delay()
|
|
|
|
def __str__(self):
|
|
return f"AutoStartStopManager-{self.name}"
|