Add safety feature_safety_manager

Rename config attribute from security_ to safety_
This commit is contained in:
Jean-Marc Collin
2024-12-31 15:42:33 +00:00
parent 7ec7d3a26a
commit 6c91c197a1
42 changed files with 795 additions and 548 deletions

View File

@@ -29,6 +29,9 @@ from .const import (
CONF_AUTO_REGULATION_EXPERT,
CONF_SHORT_EMA_PARAMS,
CONF_SAFETY_MODE,
CONF_SAFETY_DELAY_MIN,
CONF_SAFETY_MIN_ON_PERCENT,
CONF_SAFETY_DEFAULT_ON_PERCENT,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_TYPE,
CONF_USE_WINDOW_FEATURE,
@@ -291,6 +294,20 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
]:
new.pop(key, None)
# Migration 2.0 to 2.1 -> rename security parameters into safety
if config_entry.version == CONFIG_VERSION and config_entry.minor_version == 0:
for key in [
"security_delay_min",
"security_min_on_percent",
"security_default_on_percent",
]:
new_key = key.replace("security_", "safety_")
old_value = config_entry.data.get(key, None)
if old_value is not None:
new[new_key] = old_value
new.pop(key, None)
hass.config_entries.async_update_entry(
config_entry,
data=new,

View File

@@ -70,6 +70,7 @@ from .feature_presence_manager import FeaturePresenceManager
from .feature_power_manager import FeaturePowerManager
from .feature_motion_manager import FeatureMotionManager
from .feature_window_manager import FeatureWindowManager
from .feature_safety_manager import FeatureSafetyManager
_LOGGER = logging.getLogger(__name__)
@@ -102,9 +103,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"saved_preset_mode",
"saved_target_temp",
"saved_hvac_mode",
"security_delay_min",
"security_min_on_percent",
"security_default_on_percent",
"last_temperature_datetime",
"last_ext_temperature_datetime",
"minimal_activation_delay_sec",
@@ -170,11 +168,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._cur_ext_temp = None
self._should_relaunch_control_heating = None
self._security_delay_min = None
self._security_min_on_percent = None
self._security_default_on_percent = None
self._security_state = None
self._thermostat_type = None
self._attr_translation_key = "versatile_thermostat"
@@ -224,11 +217,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._power_manager: FeaturePowerManager = FeaturePowerManager(self, hass)
self._motion_manager: FeatureMotionManager = FeatureMotionManager(self, hass)
self._window_manager: FeatureWindowManager = FeatureWindowManager(self, hass)
self._safety_manager: FeatureSafetyManager = FeatureSafetyManager(self, hass)
self.register_manager(self._presence_manager)
self.register_manager(self._power_manager)
self.register_manager(self._motion_manager)
self.register_manager(self._window_manager)
self.register_manager(self._safety_manager)
self.post_init(entry_infos)
@@ -373,21 +368,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
self._tpi_coef_ext = 0
self._security_delay_min = entry_infos.get(CONF_SECURITY_DELAY_MIN)
self._security_min_on_percent = (
entry_infos.get(CONF_SECURITY_MIN_ON_PERCENT)
if entry_infos.get(CONF_SECURITY_MIN_ON_PERCENT) is not None
else DEFAULT_SECURITY_MIN_ON_PERCENT
)
self._security_default_on_percent = (
entry_infos.get(CONF_SECURITY_DEFAULT_ON_PERCENT)
if entry_infos.get(CONF_SECURITY_DEFAULT_ON_PERCENT) is not None
else DEFAULT_SECURITY_DEFAULT_ON_PERCENT
)
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
self._last_temperature_measure = self.now
self._last_ext_temperature_measure = self.now
self._security_state = False
# Initiate the ProportionalAlgorithm
if self._prop_algorithm is not None:
@@ -619,6 +602,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"saved_preset_mode", None
)
self._hvac_off_reason = old_state.attributes.get("hvac_mode_reason", None)
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
self._total_energy = old_total_energy if old_total_energy is not None else 0
_LOGGER.debug(
@@ -657,7 +642,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
_LOGGER.info(
"%s - restored state is target_temp=%.1f, preset_mode=%s, hvac_mode=%s",
"%s - restored state is target_temp=%s, preset_mode=%s, hvac_mode=%s",
self,
self._target_temp,
self._attr_preset_mode,
@@ -820,6 +805,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Return the sensor temperature."""
return self._cur_temp
@property
def current_outdoor_temperature(self) -> float | None:
"""Return the outdoor sensor temperature."""
return self._cur_ext_temp
@property
def is_aux_heat(self) -> bool | None:
"""Return true if aux heater.
@@ -861,6 +851,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Get the window manager"""
return self._window_manager
@property
def safety_manager(self) -> FeatureSafetyManager | None:
"""Get the safety manager"""
return self._safety_manager
@property
def window_state(self) -> str | None:
"""Get the window_state"""
@@ -877,9 +872,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return self._window_manager.is_window_bypass
@property
def security_state(self) -> bool | None:
"""Get the security_state"""
return self._security_state
def safety_state(self) -> str | None:
"""Get the safety_state"""
return self._safety_manager.safety_state
@property
def motion_state(self) -> str | None:
@@ -1112,8 +1107,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# I don't think we need to call async_write_ha_state if we didn't change the state
return
# In safety mode don't change preset but memorise the new expected preset when security will be off
if preset_mode != PRESET_SECURITY and self._security_state:
# In safety mode don't change preset but memorise the new expected preset when safety will be off
if preset_mode != PRESET_SAFETY and self._safety_manager.is_safety_detected:
_LOGGER.debug(
"%s - is in safety mode. Just memorise the new expected ", self
)
@@ -1182,10 +1177,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
else self._attr_min_temp
)
if preset_mode == PRESET_SECURITY:
if preset_mode == PRESET_SAFETY:
return (
self._target_temp
) # in security just keep the current target temperature, the thermostat should be off
) # in safety just keep the current target temperature, the thermostat should be off
if preset_mode == PRESET_POWER:
return self._power_manager.power_temperature
if preset_mode == PRESET_ACTIVITY:
@@ -1333,8 +1328,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
# try to restart if we were in safety mode
if self._security_state:
await self.check_safety()
if self._safety_manager.is_safety_detected:
await self._safety_manager.refresh_state()
except ValueError as err:
# La conversion a échoué, la chaîne n'est pas au format ISO 8601
@@ -1399,8 +1394,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
# try to restart if we were in safety mode
if self._security_state:
await self.check_safety()
if self._safety_manager.is_safety_detected:
await self._safety_manager.refresh_state()
# check window_auto
return await self._window_manager.manage_window_auto()
@@ -1426,8 +1421,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
# try to restart if we were in safety mode
if self._security_state:
await self.check_safety()
if self._safety_manager.is_safety_detected:
await self._safety_manager.refresh_state()
except ValueError as ex:
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
@@ -1572,163 +1567,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Get now. The local datetime or the overloaded _set_now date"""
return self._now if self._now is not None else NowClass.get_now(self._hass)
async def check_safety(self) -> bool:
"""Check if last temperature date is too long"""
now = self.now
delta_temp = (
now - self._last_temperature_measure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0
delta_ext_temp = (
now - self._last_ext_temperature_measure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0
mode_cond = self._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._security_delay_min or (
is_outdoor_checked and delta_ext_temp > self._security_delay_min
)
climate_cond: bool = self.is_over_climate and self.hvac_action not in [
HVACAction.COOLING,
HVACAction.IDLE,
]
switch_cond: bool = (
not self.is_over_climate
and self._prop_algorithm is not None
and self._prop_algorithm.calculated_on_percent
>= self._security_min_on_percent
)
_LOGGER.debug(
"%s - checking security 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 security !
shouldClimateBeInSecurity = False # temp_cond and climate_cond
shouldSwitchBeInSecurity = temp_cond and switch_cond
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
shouldStartSecurity = (
mode_cond and not self._security_state and shouldBeInSecurity
)
# attr_preset_mode is not necessary normaly. It is just here to be sure
shouldStopSecurity = (
self._security_state
and not shouldBeInSecurity
and self._attr_preset_mode == PRESET_SECURITY
)
# Logging and event
if shouldStartSecurity:
if shouldClimateBeInSecurity:
_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._security_delay_min,
delta_temp,
delta_ext_temp,
self.hvac_action,
)
elif shouldSwitchBeInSecurity:
_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._security_delay_min,
delta_temp,
delta_ext_temp,
self._prop_algorithm.on_percent * 100,
self._security_min_on_percent * 100,
)
self.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_measure": self._last_temperature_measure.replace(
tzinfo=self._current_tz
).isoformat(),
"last_ext_temperature_measure": self._last_ext_temperature_measure.replace(
tzinfo=self._current_tz
).isoformat(),
"current_temp": self._cur_temp,
"current_ext_temp": self._cur_ext_temp,
"target_temp": self.target_temperature,
},
)
# Start safety mode
if shouldStartSecurity:
self._security_state = True
self.save_hvac_mode()
self.save_preset_mode()
if self._prop_algorithm:
self._prop_algorithm.set_security(self._security_default_on_percent)
await self.async_set_preset_mode_internal(PRESET_SECURITY)
# Turn off the underlying climate or heater if security default on_percent is 0
if self.is_over_climate or self._security_default_on_percent <= 0.0:
await self.async_set_hvac_mode(HVACMode.OFF, False)
self.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_measure": self._last_temperature_measure.replace(
tzinfo=self._current_tz
).isoformat(),
"last_ext_temperature_measure": self._last_ext_temperature_measure.replace(
tzinfo=self._current_tz
).isoformat(),
"current_temp": self._cur_temp,
"current_ext_temp": self._cur_ext_temp,
"target_temp": self.target_temperature,
},
)
# Stop safety mode
if shouldStopSecurity:
_LOGGER.warning(
"%s - End of safety mode. restoring hvac_mode to %s and preset_mode to %s",
self,
self._saved_hvac_mode,
self._saved_preset_mode,
)
self._security_state = False
if self._prop_algorithm:
self._prop_algorithm.unset_security()
# Restore hvac_mode if previously saved
if self.is_over_climate or self._security_default_on_percent <= 0.0:
await self.restore_hvac_mode(False)
await self.restore_preset_mode()
self.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_measure": self._last_temperature_measure.replace(
tzinfo=self._current_tz
).isoformat(),
"last_ext_temperature_measure": self._last_ext_temperature_measure.replace(
tzinfo=self._current_tz
).isoformat(),
"current_temp": self._cur_temp,
"current_ext_temp": self._cur_ext_temp,
"target_temp": self.target_temperature,
},
)
return shouldBeInSecurity
@property
def is_initialized(self) -> bool:
"""Check if all underlyings are initialized
@@ -1740,10 +1578,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""The main function used to run the calculation at each cycle"""
_LOGGER.debug(
"%s - Checking new cycle. hvac_mode=%s, security_state=%s, preset_mode=%s",
"%s - Checking new cycle. hvac_mode=%s, safety_state=%s, preset_mode=%s",
self,
self._hvac_mode,
self._security_state,
self._safety_manager.safety_state,
self._attr_preset_mode,
)
@@ -1763,9 +1601,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.debug("%s - End of cycle (overpowering)", self)
return True
security: bool = await self.check_safety()
if security and self.is_over_climate:
_LOGGER.debug("%s - End of cycle (security and over climate)", self)
safety: bool = await self._safety_manager.refresh_state()
if safety and self.is_over_climate:
_LOGGER.debug("%s - End of cycle (safety and over climate)", self)
return True
# Stop here if we are off
@@ -1834,16 +1672,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode,
"security_delay_min": self._security_delay_min,
"security_min_on_percent": self._security_min_on_percent,
"security_default_on_percent": self._security_default_on_percent,
"last_temperature_datetime": self._last_temperature_measure.astimezone(
self._current_tz
).isoformat(),
"last_ext_temperature_datetime": self._last_ext_temperature_measure.astimezone(
self._current_tz
).isoformat(),
"security_state": self._security_state,
"minimal_activation_delay_sec": self._minimal_activation_delay,
ATTR_TOTAL_ENERGY: self.total_energy,
"last_update_datetime": self.now.isoformat(),
@@ -1956,14 +1790,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
await self.async_control_heating(force=True)
async def service_set_security(
async def SERVICE_SET_SAFETY(
self,
delay_min: int | None,
min_on_percent: float | None,
default_on_percent: float | None,
):
"""Called by a service call:
service: versatile_thermostat.set_security
service: versatile_thermostat.set_safety
data:
delay_min: 15
min_on_percent: 0.5
@@ -1972,21 +1806,23 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entity_id: climate.thermostat_2
"""
_LOGGER.info(
"%s - Calling service_set_security, delay_min: %s, min_on_percent: %s %%, default_on_percent: %s %%",
"%s - Calling SERVICE_SET_SAFETY, delay_min: %s, min_on_percent: %s %%, default_on_percent: %s %%",
self,
delay_min,
min_on_percent * 100,
default_on_percent * 100,
)
if delay_min:
self._security_delay_min = delay_min
self._safety_manager.set_safety_delay_min(delay_min)
if min_on_percent:
self._security_min_on_percent = min_on_percent
self._safety_manager.set_safety_min_on_percent(min_on_percent)
if default_on_percent:
self._security_default_on_percent = default_on_percent
self._safety_manager.set_safety_default_on_percent(default_on_percent)
if self._prop_algorithm and self._security_state:
self._prop_algorithm.set_security(self._security_default_on_percent)
if self._prop_algorithm:
self._prop_algorithm.set_safety(
self._safety_manager.safety_default_on_percent
)
await self.async_control_heating()
self.update_custom_attributes()
@@ -2070,7 +1906,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, _ in CONF_PRESETS.items():
if self.find_preset_temp(key) > 0:
preset_value = self.find_preset_temp(key)
if preset_value is not None and preset_value > 0:
self._attr_preset_modes.append(key)
_LOGGER.debug(

View File

@@ -109,7 +109,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.security_state is True
self._attr_is_on = self.my_climate.safety_manager.is_safety_detected
if old_state != self._attr_is_on:
self.async_write_ha_state()
return

View File

@@ -97,13 +97,13 @@ async def async_setup_entry(
)
platform.async_register_entity_service(
SERVICE_SET_SECURITY,
SERVICE_SET_SAFETY,
{
vol.Optional("delay_min"): cv.positive_int,
vol.Optional("min_on_percent"): vol.Coerce(float),
vol.Optional("default_on_percent"): vol.Coerce(float),
},
"service_set_security",
"SERVICE_SET_SAFETY",
)
platform.async_register_entity_service(

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
from typing import Any
import logging
import copy
from collections.abc import Mapping
from collections.abc import Mapping # pylint: disable=import-error
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError

View File

@@ -368,13 +368,13 @@ STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
STEP_CENTRAL_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_MINIMAL_ACTIVATION_DELAY, default=10): cv.positive_int,
vol.Required(CONF_SECURITY_DELAY_MIN, default=60): cv.positive_int,
vol.Required(CONF_SAFETY_DELAY_MIN, default=60): cv.positive_int,
vol.Required(
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SAFETY_MIN_ON_PERCENT,
default=DEFAULT_SECURITY_MIN_ON_PERCENT,
): vol.Coerce(float),
vol.Required(
CONF_SECURITY_DEFAULT_ON_PERCENT,
CONF_SAFETY_DEFAULT_ON_PERCENT,
default=DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
): vol.Coerce(float),
}

View File

@@ -29,7 +29,7 @@ from .prop_algorithm import (
_LOGGER = logging.getLogger(__name__)
CONFIG_VERSION = 2
CONFIG_MINOR_VERSION = 0
CONFIG_MINOR_VERSION = 1
PRESET_TEMP_SUFFIX = "_temp"
PRESET_AC_SUFFIX = "_ac"
@@ -42,10 +42,10 @@ DEVICE_MANUFACTURER = "JMCOLLIN"
DEVICE_MODEL = "Versatile Thermostat"
PRESET_POWER = "power"
PRESET_SECURITY = "security"
PRESET_SAFETY = "security"
PRESET_FROST_PROTECTION = "frost"
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SAFETY]
DOMAIN = "versatile_thermostat"
@@ -84,9 +84,9 @@ CONF_PRESET_POWER = "power_temp"
CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay"
CONF_TEMP_MIN = "temp_min"
CONF_TEMP_MAX = "temp_max"
CONF_SECURITY_DELAY_MIN = "security_delay_min"
CONF_SECURITY_MIN_ON_PERCENT = "security_min_on_percent"
CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
CONF_SAFETY_DELAY_MIN = "safety_delay_min"
CONF_SAFETY_MIN_ON_PERCENT = "safety_min_on_percent"
CONF_SAFETY_DEFAULT_ON_PERCENT = "safety_default_on_percent"
CONF_THERMOSTAT_TYPE = "thermostat_type"
CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
@@ -286,9 +286,9 @@ ALL_CONF = (
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MIN,
CONF_TEMP_MAX,
CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
CONF_SAFETY_DELAY_MIN,
CONF_SAFETY_MIN_ON_PERCENT,
CONF_SAFETY_DEFAULT_ON_PERCENT,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
@@ -374,7 +374,7 @@ SUPPORT_FLAGS = (
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
SERVICE_SET_SECURITY = "set_security"
SERVICE_SET_SAFETY = "set_safety"
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode"

View File

@@ -211,7 +211,7 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
@overrides
@property
def is_configured(self) -> bool:
"""Return True of the window feature is configured"""
"""Return True of the aiuto-start/stop feature is configured"""
return self._is_configured
@property

View File

@@ -0,0 +1,310 @@
# 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_SECURITY_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_SECURITY_DEFAULT_ON_PERCENT
)
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(
{
"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,
"safety_state": self._safety_state,
"is_safety_configured": self._is_configured,
}
)
@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}"

View File

@@ -187,7 +187,7 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
def set_security(self, default_on_percent: float):
def set_safety(self, default_on_percent: float):
"""Set a default value for on_percent (used for safety mode)"""
_LOGGER.info(
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
@@ -196,7 +196,7 @@ class PropAlgorithm:
self._default_on_percent = default_on_percent
self._calculate_internal()
def unset_security(self):
def unset_safety(self):
"""Unset the safety mode"""
_LOGGER.info(
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id

View File

@@ -76,7 +76,7 @@ set_preset_temperature:
unit_of_measurement: °
mode: slider
set_security:
set_safety:
name: Set safety
description: Change the safety parameters
target:

View File

@@ -413,7 +413,7 @@ class UnderlyingSwitch(UnderlyingEntity):
_LOGGER.debug("%s - End of cycle (3)", self)
return
# safety mode could have change the on_time percent
await self._thermostat.check_safety()
await self._thermostat.safety_manager.refresh_state()
time = self._on_time_sec
action_label = "start"