Added VT Thermometer to HACS
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
""" 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}"
|
||||
Reference in New Issue
Block a user