Files
versatile_thermostat/custom_components/versatile_thermostat/feature_safety_manager.py
Jean-Marc Collin 9e52c843bc Issue-739-refactor-to-modularize (#742)
* Refactor Presence Feature

* Add PresenceFeatureManager ok

* Python 3.13

* Fix presence test

* Refactor power feature

* Add Motion manager. All tests ok

* Tests ok. But tests are not complete

* All tests Window Feature Manager ok.

* All windows tests ok

* Fix all testus with feature_window_manager ok

* Add test_auto_start_stop feature manager. All tests ok

* Add safety feature_safety_manager
Rename config attribute from security_ to safety_

* Documentation and release

* Add safety manager direct tests

* Typo

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-01-01 16:30:18 +01:00

323 lines
12 KiB
Python

# pylint: disable=line-too-long
""" Implements the Safety as a Feature Manager"""
import logging
from typing import Any
from homeassistant.const import (
STATE_ON,
STATE_OFF,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode, HVACAction
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData
from .base_manager import BaseFeatureManager
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
class FeatureSafetyManager(BaseFeatureManager):
"""The implementation of the Safety feature"""
unrecorded_attributes = frozenset(
{
"safety_delay_min",
"safety_min_on_percent",
"safety_default_on_percent",
"is_safety_configured",
}
)
def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager"""
super().__init__(vtherm, hass)
self._is_configured: bool = False
self._safety_delay_min = None
self._safety_min_on_percent = None
self._safety_default_on_percent = None
self._safety_state = STATE_UNAVAILABLE
@overrides
def post_init(self, entry_infos: ConfigData):
"""Reinit of the manager"""
self._safety_delay_min = entry_infos.get(CONF_SAFETY_DELAY_MIN)
self._safety_min_on_percent = (
entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT)
if entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT) is not None
else DEFAULT_SAFETY_MIN_ON_PERCENT
)
self._safety_default_on_percent = (
entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT)
if entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT) is not None
else DEFAULT_SAFETY_DEFAULT_ON_PERCENT
)
if (
self._safety_delay_min is not None
and self._safety_default_on_percent is not None
and self._safety_default_on_percent is not None
):
self._safety_state = STATE_UNKNOWN
self._is_configured = True
@overrides
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 safety and an eventual action
Return True is safety should be active"""
if not self._is_configured:
_LOGGER.debug("%s - safety is disabled (or not configured)", self)
return False
now = self._vtherm.now
current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
is_safety_detected = self.is_safety_detected
delta_temp = (
now - self._vtherm.last_temperature_measure.replace(tzinfo=current_tz)
).total_seconds() / 60.0
delta_ext_temp = (
now - self._vtherm.last_ext_temperature_measure.replace(tzinfo=current_tz)
).total_seconds() / 60.0
mode_cond = self._vtherm.hvac_mode != HVACMode.OFF
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
is_outdoor_checked = (
not api.safety_mode
or api.safety_mode.get("check_outdoor_sensor") is not False
)
temp_cond: bool = delta_temp > self._safety_delay_min or (
is_outdoor_checked and delta_ext_temp > self._safety_delay_min
)
climate_cond: bool = (
self._vtherm.is_over_climate
and self._vtherm.hvac_action
not in [
HVACAction.COOLING,
HVACAction.IDLE,
]
)
switch_cond: bool = (
not self._vtherm.is_over_climate
and self._vtherm.proportional_algorithm is not None
and self._vtherm.proportional_algorithm.calculated_on_percent
>= self._safety_min_on_percent
)
_LOGGER.debug(
"%s - checking safety delta_temp=%.1f delta_ext_temp=%.1f mod_cond=%s temp_cond=%s climate_cond=%s switch_cond=%s",
self,
delta_temp,
delta_ext_temp,
mode_cond,
temp_cond,
climate_cond,
switch_cond,
)
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in safety !
should_climate_be_in_security = False # temp_cond and climate_cond
should_switch_be_in_security = temp_cond and switch_cond
should_be_in_security = (
should_climate_be_in_security or should_switch_be_in_security
)
should_start_security = (
mode_cond and not is_safety_detected and should_be_in_security
)
# attr_preset_mode is not necessary normaly. It is just here to be sure
should_stop_security = (
is_safety_detected
and not should_be_in_security
and self._vtherm.preset_mode == PRESET_SAFETY
)
# Logging and event
if should_start_security:
if should_climate_be_in_security:
_LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Setting it into safety mode",
self,
self._safety_delay_min,
delta_temp,
delta_ext_temp,
self.hvac_action,
)
elif should_switch_be_in_security:
_LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f %%) is over defined value (%.2f %%). Set it into safety mode",
self,
self._safety_delay_min,
delta_temp,
delta_ext_temp,
self._vtherm.proportional_algorithm.on_percent * 100,
self._safety_min_on_percent * 100,
)
self._vtherm.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
tzinfo=current_tz
).isoformat(),
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
tzinfo=current_tz
).isoformat(),
"current_temp": self._vtherm.current_temperature,
"current_ext_temp": self._vtherm.current_outdoor_temperature,
"target_temp": self._vtherm.target_temperature,
},
)
# Start safety mode
if should_start_security:
self._safety_state = STATE_ON
self._vtherm.save_hvac_mode()
self._vtherm.save_preset_mode()
if self._vtherm.proportional_algorithm:
self._vtherm.proportional_algorithm.set_safety(
self._safety_default_on_percent
)
await self._vtherm.async_set_preset_mode_internal(PRESET_SAFETY)
# Turn off the underlying climate or heater if safety default on_percent is 0
if self._vtherm.is_over_climate or self._safety_default_on_percent <= 0.0:
await self._vtherm.async_set_hvac_mode(HVACMode.OFF, False)
self._vtherm.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
tzinfo=current_tz
).isoformat(),
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
tzinfo=current_tz
).isoformat(),
"current_temp": self._vtherm.current_temperature,
"current_ext_temp": self._vtherm.current_outdoor_temperature,
"target_temp": self._vtherm.target_temperature,
},
)
# Stop safety mode
elif should_stop_security:
_LOGGER.warning(
"%s - End of safety mode. restoring hvac_mode to %s and preset_mode to %s",
self,
self._vtherm.saved_hvac_mode,
self._vtherm.saved_preset_mode,
)
self._safety_state = STATE_OFF
if self._vtherm.proportional_algorithm:
self._vtherm.proportional_algorithm.unset_safety()
# Restore hvac_mode if previously saved
if self._vtherm.is_over_climate or self._safety_default_on_percent <= 0.0:
await self._vtherm.restore_hvac_mode(False)
await self._vtherm.restore_preset_mode()
self._vtherm.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
tzinfo=current_tz
).isoformat(),
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
tzinfo=current_tz
).isoformat(),
"current_temp": self._vtherm.current_temperature,
"current_ext_temp": self._vtherm.current_outdoor_temperature,
"target_temp": self._vtherm.target_temperature,
},
)
# Initialize the safety_state if not already done
elif not should_be_in_security and self._safety_state in [STATE_UNKNOWN]:
self._safety_state = STATE_OFF
return should_be_in_security
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
"""Add some custom attributes"""
extra_state_attributes.update(
{
"is_safety_configured": self._is_configured,
"safety_state": self._safety_state,
}
)
if self._is_configured:
extra_state_attributes.update(
{
"safety_delay_min": self._safety_delay_min,
"safety_min_on_percent": self._safety_min_on_percent,
"safety_default_on_percent": self._safety_default_on_percent,
}
)
@overrides
@property
def is_configured(self) -> bool:
"""Return True of the safety feature is configured"""
return self._is_configured
def set_safety_delay_min(self, safety_delay_min):
"""Set the delay min"""
self._safety_delay_min = safety_delay_min
def set_safety_min_on_percent(self, safety_min_on_percent):
"""Set the min on percent"""
self._safety_min_on_percent = safety_min_on_percent
def set_safety_default_on_percent(self, safety_default_on_percent):
"""Set the default on_percent"""
self._safety_default_on_percent = safety_default_on_percent
@property
def is_safety_detected(self) -> bool:
"""Returns the is vtherm is in safety mode"""
return self._safety_state == STATE_ON
@property
def safety_state(self) -> str:
"""Returns the safety state: STATE_ON, STATE_OFF, STATE_UNKWNON, STATE_UNAVAILABLE"""
return self._safety_state
@property
def safety_delay_min(self) -> bool:
"""Returns the safety delay min"""
return self._safety_delay_min
@property
def safety_min_on_percent(self) -> bool:
"""Returns the safety min on percent"""
return self._safety_min_on_percent
@property
def safety_default_on_percent(self) -> bool:
"""Returns the safety safety_default_on_percent"""
return self._safety_default_on_percent
def __str__(self):
return f"SafetyManager-{self.name}"