diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 34c1c1b..13167c1 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,2 +1,2 @@
-FROM mcr.microsoft.com/devcontainers/python:1-3.12
-RUN apt update && apt install -y ffmpeg
+FROM mcr.microsoft.com/devcontainers/python:1-3.13
+RUN apt update && apt install -y ffmpeg libturbojpeg0-dev
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 3e842c2..cb4393d 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -37,7 +37,7 @@
"yzhang.markdown-all-in-one",
"github.vscode-github-actions",
"azuretools.vscode-docker",
- "huizhou.githd",
+ "huizhou.githd"
],
"settings": {
"files.eol": "\n",
diff --git a/.devcontainer/pytest.ini b/.devcontainer/pytest.ini
new file mode 100644
index 0000000..6a7d170
--- /dev/null
+++ b/.devcontainer/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+asyncio_default_fixture_loop_scope = function
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md
index 020a3ce..0396265 100644
--- a/.github/ISSUE_TEMPLATE/issue.md
+++ b/.github/ISSUE_TEMPLATE/issue.md
@@ -69,10 +69,10 @@ motion_state: 'off'
overpowering_state: false
presence_state: 'on'
window_auto_state: false
-window_bypass_state: false
-security_delay_min: 2
-security_min_on_percent: 0.5
-security_default_on_percent: 0.1
+is_window_bypass: false
+safety_delay_min: 2
+safety_min_on_percent: 0.5
+safety_default_on_percent: 0.1
last_temperature_datetime: '2023-11-05T00:48:54.873157+01:00'
last_ext_temperature_datetime: '2023-11-05T00:48:53.240122+01:00'
security_state: true
diff --git a/custom_components/versatile_thermostat/__init__.py b/custom_components/versatile_thermostat/__init__.py
index 4dbe3e6..e0841ea 100644
--- a/custom_components/versatile_thermostat/__init__.py
+++ b/custom_components/versatile_thermostat/__init__.py
@@ -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,
diff --git a/custom_components/versatile_thermostat/base_entity.py b/custom_components/versatile_thermostat/base_entity.py
new file mode 100644
index 0000000..34f8d3d
--- /dev/null
+++ b/custom_components/versatile_thermostat/base_entity.py
@@ -0,0 +1,118 @@
+""" A base class for all VTherm entities"""
+
+import logging
+from datetime import timedelta
+from homeassistant.core import HomeAssistant, callback, Event
+from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
+from homeassistant.helpers.event import async_track_state_change_event, async_call_later
+
+
+from .const import DOMAIN, DEVICE_MANUFACTURER
+
+from .base_thermostat import BaseThermostat
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class VersatileThermostatBaseEntity(Entity):
+ """A base class for all entities"""
+
+ _my_climate: BaseThermostat
+ hass: HomeAssistant
+ _config_id: str
+ _device_name: str
+
+ def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
+ """The CTOR"""
+ self.hass = hass
+ self._config_id = config_id
+ self._device_name = device_name
+ self._my_climate = None
+ self._cancel_call = None
+ self._attr_has_entity_name = True
+
+ @property
+ def should_poll(self) -> bool:
+ """Do not poll for those entities"""
+ return False
+
+ @property
+ def my_climate(self) -> BaseThermostat | None:
+ """Returns my climate if found"""
+ if not self._my_climate:
+ self._my_climate = self.find_my_versatile_thermostat()
+ if self._my_climate:
+ # Only the first time
+ self.my_climate_is_initialized()
+ return self._my_climate
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return the device info."""
+ return DeviceInfo(
+ entry_type=DeviceEntryType.SERVICE,
+ identifiers={(DOMAIN, self._config_id)},
+ name=self._device_name,
+ manufacturer=DEVICE_MANUFACTURER,
+ model=DOMAIN,
+ )
+
+ def find_my_versatile_thermostat(self) -> BaseThermostat:
+ """Find the underlying climate entity"""
+ try:
+ component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
+ for entity in component.entities:
+ # _LOGGER.debug("Device_info is %s", entity.device_info)
+ if entity.device_info == self.device_info:
+ _LOGGER.debug("Found %s!", entity)
+ return entity
+ except KeyError:
+ pass
+
+ return None
+
+ @callback
+ async def async_added_to_hass(self):
+ """Listen to my climate state change"""
+
+ # Check delay condition
+ async def try_find_climate(_):
+ _LOGGER.debug(
+ "%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
+ )
+ mcl = self.my_climate
+ if mcl:
+ if self._cancel_call:
+ self._cancel_call()
+ self._cancel_call = None
+ self.async_on_remove(
+ async_track_state_change_event(
+ self.hass,
+ [mcl.entity_id],
+ self.async_my_climate_changed,
+ )
+ )
+ else:
+ _LOGGER.debug("%s - no entity to listen. Try later", self)
+ self._cancel_call = async_call_later(
+ self.hass, timedelta(seconds=1), try_find_climate
+ )
+
+ await try_find_climate(None)
+
+ @callback
+ def my_climate_is_initialized(self):
+ """Called when the associated climate is initialized"""
+ return
+
+ @callback
+ async def async_my_climate_changed(
+ self, event: Event
+ ): # pylint: disable=unused-argument
+ """Called when my climate have change
+ This method aims to be overriden to take the status change
+ """
+ return
diff --git a/custom_components/versatile_thermostat/base_manager.py b/custom_components/versatile_thermostat/base_manager.py
new file mode 100644
index 0000000..a2c6975
--- /dev/null
+++ b/custom_components/versatile_thermostat/base_manager.py
@@ -0,0 +1,58 @@
+""" Implements a base Feature Manager for Versatile Thermostat """
+
+import logging
+from typing import Any
+
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant
+
+from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData
+
+from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class BaseFeatureManager:
+ """A base class for all feature"""
+
+ def __init__(self, vtherm: Any, hass: HomeAssistant):
+ """Init of a featureManager"""
+ self._vtherm = vtherm
+ self._name = vtherm.name
+ self._active_listener: list[CALLBACK_TYPE] = []
+ self._hass = hass
+
+ def post_init(self, entry_infos: ConfigData):
+ """Initialize the attributes of the FeatureManager"""
+ raise NotImplementedError()
+
+ def start_listening(self):
+ """Start listening the underlying entity"""
+ raise NotImplementedError()
+
+ def stop_listening(self) -> bool:
+ """stop listening to the sensor"""
+ while self._active_listener:
+ self._active_listener.pop()()
+
+ self._active_listener = []
+
+ def add_listener(self, func: CALLBACK_TYPE) -> None:
+ """Add a listener to the list of active listener"""
+ self._active_listener.append(func)
+
+ @property
+ def is_configured(self) -> bool:
+ """True if the FeatureManager is fully configured"""
+ raise NotImplementedError()
+
+ @property
+ def name(self) -> str:
+ """The name"""
+ return self._name
+
+ @property
+ def hass(self) -> HomeAssistant:
+ """The HA instance"""
+ return self._hass
diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py
index 25b3e08..ae88178 100644
--- a/custom_components/versatile_thermostat/base_thermostat.py
+++ b/custom_components/versatile_thermostat/base_thermostat.py
@@ -4,10 +4,7 @@
""" Implements the VersatileThermostat climate component """
import math
import logging
-
-from datetime import timedelta, datetime
-from types import MappingProxyType
-from typing import Any, TypeVar, Generic
+from typing import Any, Generic
from homeassistant.core import (
HomeAssistant,
@@ -28,12 +25,8 @@ from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import (
async_track_state_change_event,
- async_call_later,
- EventStateChangedData,
)
-from homeassistant.exceptions import ConditionError
-from homeassistant.helpers import condition
from homeassistant.components.climate import (
ATTR_PRESET_MODE,
@@ -58,13 +51,11 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
- STATE_OFF,
STATE_ON,
- STATE_HOME,
- STATE_NOT_HOME,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData, T
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -72,12 +63,16 @@ from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingEntity
from .prop_algorithm import PropAlgorithm
-from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import ExponentialMovingAverage
+from .base_manager import BaseFeatureManager
+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__)
-ConfigData = MappingProxyType[str, Any]
-T = TypeVar("T", bound=UnderlyingEntity)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device."""
@@ -104,31 +99,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"comfort_away_temp",
"power_temp",
"ac_mode",
- "current_power_max",
+ "current_max_power",
"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",
- "device_power",
- "mean_cycle_power",
"last_update_datetime",
"timezone",
- "window_sensor_entity_id",
- "window_delay_sec",
- "window_auto_enabled",
- "window_auto_open_threshold",
- "window_auto_close_threshold",
- "window_auto_max_duration",
- "window_action",
- "motion_sensor_entity_id",
- "presence_sensor_entity_id",
- "power_sensor_entity_id",
- "max_power_sensor_entity_id",
"temperature_unit",
"is_device_active",
"device_actives",
@@ -141,6 +120,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
}
)
)
+ .union(FeaturePresenceManager.unrecorded_attributes)
+ .union(FeaturePowerManager.unrecorded_attributes)
+ .union(FeatureMotionManager.unrecorded_attributes)
+ .union(FeatureWindowManager.unrecorded_attributes)
)
def __init__(
@@ -172,13 +155,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._fan_mode = None
self._humidity = None
self._swing_mode = None
- self._current_power = None
- self._current_power_max = None
- self._window_state = None
- self._motion_state = None
+
self._saved_hvac_mode = None
- self._window_call_cancel = None
- self._motion_call_cancel = None
+
self._cur_temp = None
self._ac_mode = None
self._temp_sensor_entity_id = None
@@ -187,15 +166,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._last_ext_temperature_measure = None
self._last_temperature_measure = None
self._cur_ext_temp = None
- self._presence_state = None
- self._overpowering_state = 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"
@@ -207,17 +179,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._underlying_climate_start_hvac_action_date = None
self._underlying_climate_delta_t = 0
- self._window_sensor_entity_id = None
- self._window_delay_sec = None
- self._window_auto_open_threshold = 0
- self._window_auto_close_threshold = 0
- self._window_auto_max_duration = 0
- self._window_auto_state = False
- self._window_auto_on = False
- self._window_auto_algo = None
- self._window_bypass_state = False
- self._window_action = None
-
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
# Last change time is the datetime of the last change sent by VTherm to the device
@@ -247,8 +208,29 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_off_reason: HVAC_OFF_REASONS | None = None
+ # Instanciate all features manager
+ self._managers: list[BaseFeatureManager] = []
+
+ self._presence_manager: FeaturePresenceManager = FeaturePresenceManager(
+ self, hass
+ )
+ 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)
+ def register_manager(self, manager: BaseFeatureManager):
+ """Register a manager"""
+ self._managers.append(manager)
+
def clean_central_config_doublon(
self, config_entry: ConfigData, central_config: ConfigEntry | None
) -> dict[str, Any]:
@@ -311,6 +293,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._entry_infos = entry_infos
+ # Post init all managers
+ for manager in self._managers:
+ manager.post_init(entry_infos)
+
self._use_central_config_temperature = entry_infos.get(
CONF_USE_PRESETS_CENTRAL_CONFIG
) or (
@@ -326,13 +312,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_preset_modes: list[str] | None
- if self._window_call_cancel is not None:
- self._window_call_cancel()
- self._window_call_cancel = None
- if self._motion_call_cancel is not None:
- self._motion_call_cancel()
- self._motion_call_cancel = None
-
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
# Initialize underlying entities (will be done in subclasses)
@@ -344,55 +323,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
CONF_LAST_SEEN_TEMP_SENSOR
)
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
- self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
- self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
- self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
- self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
-
- self._window_auto_open_threshold = entry_infos.get(
- CONF_WINDOW_AUTO_OPEN_THRESHOLD
- )
- self._window_auto_close_threshold = entry_infos.get(
- CONF_WINDOW_AUTO_CLOSE_THRESHOLD
- )
- self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
- self._window_auto_on = (
- self._window_sensor_entity_id is None
- and self._window_auto_open_threshold is not None
- and self._window_auto_open_threshold > 0.0
- and self._window_auto_close_threshold is not None
- and self._window_auto_max_duration is not None
- and self._window_auto_max_duration > 0
- )
- self._window_auto_state = False
- self._window_auto_algo = WindowOpenDetectionAlgorithm(
- alert_threshold=self._window_auto_open_threshold,
- end_alert_threshold=self._window_auto_close_threshold,
- )
-
- self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
- self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
- self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY)
- if not self._motion_off_delay_sec:
- self._motion_off_delay_sec = self._motion_delay_sec
-
- self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
- self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
- self._motion_on = (
- self._motion_sensor_entity_id is not None
- and self._motion_preset is not None
- and self._no_motion_preset is not None
- )
self._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT)
self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT)
- self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
- self._power_temp = entry_infos.get(CONF_PRESET_POWER)
-
- self._presence_on = (
- entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
- and self._presence_sensor_entity_id is not None
- )
if self._ac_mode:
# Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
@@ -416,20 +349,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_preset_mode = PRESET_NONE
self._saved_preset_mode = PRESET_NONE
- # Power management
- self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
- self._pmax_on = False
- self._current_power = None
- self._current_power_max = None
- if (
- self._max_power_sensor_entity_id
- and self._power_sensor_entity_id
- and self._device_power
- ):
- self._pmax_on = True
- else:
- _LOGGER.info("%s - Power management is not fully configured", self)
-
# will be restored if possible
self._target_temp = None
self._saved_target_temp = PRESET_NONE
@@ -449,32 +368,14 @@ 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:
del self._prop_algorithm
- # Memory synthesis state
- self._motion_state = None
- self._window_state = None
- self._overpowering_state = None
- self._presence_state = None
-
self._total_energy = None
_LOGGER.debug("%s - post_init_ resetting energy to None", self)
@@ -501,10 +402,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entry_infos.get(CONF_USED_BY_CENTRAL_BOILER) is True
)
- self._window_action = (
- entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
- )
-
self._max_on_percent = api.max_on_percent
_LOGGER.debug(
@@ -545,49 +442,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
)
- if self._window_sensor_entity_id:
- self.async_on_remove(
- async_track_state_change_event(
- self.hass,
- [self._window_sensor_entity_id],
- self._async_windows_changed,
- )
- )
- if self._motion_sensor_entity_id:
- self.async_on_remove(
- async_track_state_change_event(
- self.hass,
- [self._motion_sensor_entity_id],
- self._async_motion_changed,
- )
- )
-
- if self._power_sensor_entity_id:
- self.async_on_remove(
- async_track_state_change_event(
- self.hass,
- [self._power_sensor_entity_id],
- self._async_power_changed,
- )
- )
-
- if self._max_power_sensor_entity_id:
- self.async_on_remove(
- async_track_state_change_event(
- self.hass,
- [self._max_power_sensor_entity_id],
- self._async_max_power_changed,
- )
- )
-
- if self._presence_on:
- self.async_on_remove(
- async_track_state_change_event(
- self.hass,
- [self._presence_sensor_entity_id],
- self._async_presence_changed,
- )
- )
+ # start listening for all managers
+ for manager in self._managers:
+ manager.start_listening()
self.async_on_remove(self.remove_thermostat)
@@ -606,6 +463,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self)
+ # stop listening for all managers
+ for manager in self._managers:
+ manager.stop_listening()
+
for under in self._underlyings:
under.remove_entity()
@@ -662,82 +523,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self,
)
- if self._pmax_on:
- # try to acquire current power and power max
- current_power_state = self.hass.states.get(self._power_sensor_entity_id)
- if current_power_state and current_power_state.state not in (
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- ):
- self._current_power = float(current_power_state.state)
- _LOGGER.debug(
- "%s - Current power have been retrieved: %.3f",
- self,
- self._current_power,
- )
- need_write_state = True
-
- # Try to acquire power max
- current_power_max_state = self.hass.states.get(
- self._max_power_sensor_entity_id
- )
- if current_power_max_state and current_power_max_state.state not in (
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- ):
- self._current_power_max = float(current_power_max_state.state)
- _LOGGER.debug(
- "%s - Current power max have been retrieved: %.3f",
- self,
- self._current_power_max,
- )
- need_write_state = True
-
- # try to acquire window entity state
- if self._window_sensor_entity_id:
- window_state = self.hass.states.get(self._window_sensor_entity_id)
- if window_state and window_state.state not in (
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- ):
- self._window_state = window_state.state == STATE_ON
- _LOGGER.debug(
- "%s - Window state have been retrieved: %s",
- self,
- self._window_state,
- )
- need_write_state = True
-
- # try to acquire motion entity state
- if self._motion_sensor_entity_id:
- motion_state = self.hass.states.get(self._motion_sensor_entity_id)
- if motion_state and motion_state.state not in (
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- ):
- self._motion_state = motion_state.state
- _LOGGER.debug(
- "%s - Motion state have been retrieved: %s",
- self,
- self._motion_state,
- )
- # recalculate the right target_temp in activity mode
- await self._async_update_motion_temp()
- need_write_state = True
-
- if self._presence_on:
- # try to acquire presence entity state
- presence_state = self.hass.states.get(self._presence_sensor_entity_id)
- if presence_state and presence_state.state not in (
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- ):
- await self._async_update_presence(presence_state.state)
- _LOGGER.debug(
- "%s - Presence have been retrieved: %s",
- self,
- presence_state.state,
- )
+ # refresh states for all managers
+ for manager in self._managers:
+ if await manager.refresh_state():
need_write_state = True
if need_write_state:
@@ -776,16 +564,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# If we have a previously saved temperature
if old_state.attributes.get(ATTR_TEMPERATURE) is None:
if self._ac_mode:
- await self._async_internal_set_temperature(self.max_temp)
+ await self.change_target_temperature(self.max_temp)
else:
- await self._async_internal_set_temperature(self.min_temp)
+ await self.change_target_temperature(self.min_temp)
_LOGGER.warning(
"%s - Undefined target temperature, falling back to %s",
self,
self._target_temp,
)
else:
- await self._async_internal_set_temperature(
+ await self.change_target_temperature(
float(old_state.attributes[ATTR_TEMPERATURE])
)
@@ -808,12 +596,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
]:
self._hvac_mode = old_state.state
- # restpre also saved info so that window detection will work
+ # restore also saved info so that window detection will work
self._saved_hvac_mode = old_state.attributes.get("saved_hvac_mode", None)
self._saved_preset_mode = old_state.attributes.get(
"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(
@@ -827,9 +617,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# No previous state, try and restore defaults
if self._target_temp is None:
if self._ac_mode:
- await self._async_internal_set_temperature(self.max_temp)
+ await self.change_target_temperature(self.max_temp)
else:
- await self._async_internal_set_temperature(self.min_temp)
+ await self.change_target_temperature(self.min_temp)
_LOGGER.warning(
"No previously saved temperature, setting to %s", self._target_temp
)
@@ -846,13 +636,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if not self.is_on and self.hvac_off_reason is None:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
- self._saved_target_temp = self._target_temp
+ self.save_target_temp()
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
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,
@@ -1015,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.
@@ -1023,14 +818,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""
return None
- @property
- def mean_cycle_power(self) -> float | None:
- """Returns the mean power consumption during the cycle"""
- if not self._device_power:
- return None
-
- return float(self._device_power * self._prop_algorithm.on_percent)
-
@property
def total_energy(self) -> float | None:
"""Returns the total energy calculated for this thermostast"""
@@ -1039,50 +826,65 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
else:
return None
- @property
- def device_power(self) -> float | None:
- """Returns the device_power for this thermostast"""
- return self._device_power
-
@property
def overpowering_state(self) -> bool | None:
"""Get the overpowering_state"""
- return self._overpowering_state
+ return self._power_manager.overpowering_state
+
+ @property
+ def power_manager(self) -> FeaturePowerManager | None:
+ """Get the power manager"""
+ return self._power_manager
+
+ @property
+ def presence_manager(self) -> FeaturePresenceManager | None:
+ """Get the presence manager"""
+ return self._presence_manager
+
+ @property
+ def motion_manager(self) -> FeatureMotionManager | None:
+ """Get the motion manager"""
+ return self._motion_manager
+
+ @property
+ def window_manager(self) -> FeatureWindowManager | None:
+ """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"""
- return STATE_ON if self._window_state else STATE_OFF
+ return self._window_manager.window_state
@property
def window_auto_state(self) -> str | None:
"""Get the window_auto_state"""
- return STATE_ON if self._window_auto_state else STATE_OFF
+ return self._window_manager.window_auto_state
@property
- def window_bypass_state(self) -> bool | None:
+ def is_window_bypass(self) -> bool | None:
"""Get the Window Bypass"""
- return self._window_bypass_state
+ return self._window_manager.is_window_bypass
@property
- def window_action(self) -> bool | None:
- """Get the Window Action"""
- return self._window_action
+ def safety_state(self) -> str | None:
+ """Get the safety_state"""
+ return self._safety_manager.safety_state
@property
- def security_state(self) -> bool | None:
- """Get the security_state"""
- return self._security_state
-
- @property
- def motion_state(self) -> bool | None:
+ def motion_state(self) -> str | None:
"""Get the motion_state"""
- return self._motion_state
+ return self._motion_manager.motion_state
@property
- def presence_state(self) -> bool | None:
+ def presence_state(self) -> str | None:
"""Get the presence_state"""
- return self._presence_state
+ return self._presence_manager.presence_state
@property
def proportional_algorithm(self) -> PropAlgorithm | None:
@@ -1101,10 +903,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property
def preset_mode(self) -> str | None:
- """Return the current preset mode, e.g., home, away, temp.
-
- Requires ClimateEntityFeature.PRESET_MODE.
- """
+ """Return the current preset mode comfort, eco, boost,...,"""
return self._attr_preset_mode
@property
@@ -1117,15 +916,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property
def last_temperature_slope(self) -> float | None:
"""Return the last temperature slope curve if any"""
- if not self._window_auto_algo:
- return None
- else:
- return self._window_auto_algo.last_slope
-
- @property
- def is_window_auto_enabled(self) -> bool:
- """True if the Window auto feature is enabled"""
- return self._window_auto_on
+ return self._window_manager.last_slope
@property
def nb_underlying_entities(self) -> int:
@@ -1248,9 +1039,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
if self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL] and self.preset_mode != PRESET_NONE:
if self.preset_mode != PRESET_FROST_PROTECTION:
- await self._async_set_preset_mode_internal(self.preset_mode, True)
+ await self.async_set_preset_mode_internal(self.preset_mode, True)
else:
- await self._async_set_preset_mode_internal(PRESET_ECO, True, False)
+ await self.async_set_preset_mode_internal(PRESET_ECO, True, False)
if need_control_heating and sub_need_control_heating:
await self.async_control_heating(force=True)
@@ -1293,12 +1084,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return
- await self._async_set_preset_mode_internal(
+ await self.async_set_preset_mode_internal(
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
)
await self.async_control_heating(force=True)
- async def _async_set_preset_mode_internal(
+ async def async_set_preset_mode_internal(
self, preset_mode: str, force=False, overwrite_saved_preset=True
):
"""Set new preset mode."""
@@ -1316,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
)
@@ -1325,24 +1116,22 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_preset_mode = preset_mode
return
- old_preset_mode = self._attr_preset_mode
+ # Remove this old_preset_mode = self._attr_preset_mode
recalculate = True
if preset_mode == PRESET_NONE:
self._attr_preset_mode = PRESET_NONE
if self._saved_target_temp:
- await self._async_internal_set_temperature(self._saved_target_temp)
+ await self.restore_target_temp()
elif preset_mode == PRESET_ACTIVITY:
self._attr_preset_mode = PRESET_ACTIVITY
- await self._async_update_motion_temp()
+ await self._motion_manager.update_motion_state(None, False)
else:
if self._attr_preset_mode == PRESET_NONE:
- self._saved_target_temp = self._target_temp
+ self.save_target_temp()
self._attr_preset_mode = preset_mode
# Switch the temperature if window is not 'on'
- if self.window_state != STATE_ON:
- await self._async_internal_set_temperature(
- self.find_preset_temp(preset_mode)
- )
+ if not self._window_manager.is_window_detected:
+ await self.change_target_temperature(self.find_preset_temp(preset_mode))
else:
# Window is on, so we just save the new expected temp
# so that closing the window will restore it
@@ -1388,28 +1177,19 @@ 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_temp
+ return self._power_manager.power_temperature
if preset_mode == PRESET_ACTIVITY:
+ motion_preset = self._motion_manager.get_current_motion_preset()
if self._ac_mode and self._hvac_mode == HVACMode.COOL:
- motion_preset = (
- self._motion_preset + PRESET_AC_SUFFIX
- if self._motion_state == STATE_ON
- else self._no_motion_preset + PRESET_AC_SUFFIX
- )
- else:
- motion_preset = (
- self._motion_preset
- if self._motion_state == STATE_ON
- else self._no_motion_preset
- )
+ motion_preset = motion_preset + PRESET_AC_SUFFIX
if motion_preset in self._presets:
- if self._presence_on and self.presence_state in [STATE_OFF, None]:
+ if self._presence_manager.is_absence_detected:
return self._presets_away[motion_preset + PRESET_AWAY_SUFFIX]
else:
return self._presets[motion_preset]
@@ -1423,13 +1203,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.info("%s - find preset temp: %s", self, preset_mode)
temp_val = self._presets.get(preset_mode, 0)
- if not self._presence_on or self._presence_state in [
- None,
- STATE_ON,
- STATE_HOME,
- ]:
- return temp_val
- else:
+ # if not self._presence_on or self._presence_state in [
+ # None,
+ # STATE_ON,
+ # STATE_HOME,
+ # ]:
+ if self._presence_manager.is_absence_detected:
# We should return the preset_away temp val but if
# preset temp is 0, that means the user don't want to use
# the preset so we return 0, even if there is a value is preset_away
@@ -1438,6 +1217,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if temp_val > 0
else temp_val
)
+ else:
+ return temp_val
def get_preset_away_name(self, preset_mode: str) -> str:
"""Get the preset name in away mode (when presence is off)"""
@@ -1466,15 +1247,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return
self._attr_preset_mode = PRESET_NONE
- if self.window_state != STATE_ON:
- await self._async_internal_set_temperature(temperature)
+ if not self._window_manager.is_window_detected:
+ await self.change_target_temperature(temperature)
self.recalculate()
self.reset_last_change_time_from_vtherm()
await self.async_control_heating(force=True)
else:
self._saved_target_temp = temperature
- async def _async_internal_set_temperature(self, temperature: float):
+ async def change_target_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any"""
if temperature:
self._target_temp = temperature
@@ -1503,8 +1284,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data)
@callback
- async def _async_temperature_changed(self, event: Event):
- """Handle temperature of the temperature sensor changes."""
+ async def _async_temperature_changed(self, event: Event) -> callable:
+ """Handle temperature of the temperature sensor changes.
+ Return the fonction to desarm (clear) the window auto check"""
new_state: State = event.data.get("new_state")
_LOGGER.debug(
"%s - Temperature changed. Event.new_state is %s",
@@ -1546,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
@@ -1573,203 +1355,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.recalculate()
await self.async_control_heating(force=False)
- @callback
- async def _async_windows_changed(self, event):
- """Handle window changes."""
- new_state = event.data.get("new_state")
- old_state = event.data.get("old_state")
- _LOGGER.info(
- "%s - Window changed. Event.new_state is %s, _hvac_mode=%s, _saved_hvac_mode=%s",
- self,
- new_state,
- self._hvac_mode,
- self._saved_hvac_mode,
- )
-
- # Check delay condition
- async def try_window_condition(_):
- try:
- long_enough = condition.state(
- self.hass,
- self._window_sensor_entity_id,
- new_state.state,
- timedelta(seconds=self._window_delay_sec),
- )
- except ConditionError:
- long_enough = False
-
- if not long_enough:
- _LOGGER.debug(
- "Window delay condition is not satisfied. Ignore window event"
- )
- self._window_state = old_state.state == STATE_ON
- return
-
- _LOGGER.debug("%s - Window delay condition is satisfied", self)
- # if not self._saved_hvac_mode:
- # self._saved_hvac_mode = self._hvac_mode
-
- if self._window_state == (new_state.state == STATE_ON):
- _LOGGER.debug("%s - no change in window state. Forget the event")
- return
-
- self._window_state = new_state.state == STATE_ON
-
- _LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
- if self._window_bypass_state:
- _LOGGER.info(
- "%s - Window ByPass is activated. Ignore window event", self
- )
- else:
- await self.change_window_detection_state(self._window_state)
-
- self.update_custom_attributes()
-
- if new_state is None or old_state is None or new_state.state == old_state.state:
- return try_window_condition
-
- if self._window_call_cancel:
- self._window_call_cancel()
- self._window_call_cancel = None
- self._window_call_cancel = async_call_later(
- self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition
- )
- # For testing purpose we need to access the inner function
- return try_window_condition
-
- @callback
- async def _async_motion_changed(self, event):
- """Handle motion changes."""
- new_state = event.data.get("new_state")
- _LOGGER.info(
- "%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
- self,
- new_state,
- self._attr_preset_mode,
- PRESET_ACTIVITY,
- )
-
- if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
- return
-
- # Check delay condition
- async def try_motion_condition(_):
- try:
- delay = (
- self._motion_delay_sec
- if new_state.state == STATE_ON
- else self._motion_off_delay_sec
- )
- long_enough = condition.state(
- self.hass,
- self._motion_sensor_entity_id,
- new_state.state,
- timedelta(seconds=delay),
- )
- except ConditionError:
- long_enough = False
-
- if not long_enough:
- _LOGGER.debug(
- "Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
- )
- # Get sensor current state
- motion_state = self.hass.states.get(self._motion_sensor_entity_id)
- _LOGGER.debug(
- "%s - motion_state=%s, new_state.state=%s",
- self,
- motion_state.state,
- new_state.state,
- )
- if (
- motion_state.state == new_state.state
- and new_state.state == STATE_ON
- ):
- _LOGGER.debug(
- "%s - the motion sensor is finally 'on' after the delay", self
- )
- long_enough = True
- else:
- long_enough = False
-
- if long_enough:
- _LOGGER.debug("%s - Motion delay condition is satisfied", self)
- self._motion_state = new_state.state
- if self._attr_preset_mode == PRESET_ACTIVITY:
-
- new_preset = (
- self._motion_preset
- if self._motion_state == STATE_ON
- else self._no_motion_preset
- )
- _LOGGER.info(
- "%s - Motion condition have changes. New preset temp will be %s",
- self,
- new_preset,
- )
- # We do not change the preset which is kept to ACTIVITY but only the target_temperature
- # We take the presence into account
-
- await self._async_internal_set_temperature(
- self.find_preset_temp(new_preset)
- )
- self.recalculate()
- await self.async_control_heating(force=True)
- else:
- self._motion_state = (
- STATE_ON if new_state.state == STATE_OFF else STATE_OFF
- )
-
- self._motion_call_cancel = None
-
- im_on = self._motion_state == STATE_ON
- delay_running = self._motion_call_cancel is not None
- event_on = new_state.state == STATE_ON
-
- def arm():
- """Arm the timer"""
- delay = (
- self._motion_delay_sec
- if new_state.state == STATE_ON
- else self._motion_off_delay_sec
- )
- self._motion_call_cancel = async_call_later(
- self.hass, timedelta(seconds=delay), try_motion_condition
- )
-
- def desarm():
- # restart the timer
- self._motion_call_cancel()
- self._motion_call_cancel = None
-
- # if I'm off
- if not im_on:
- if event_on and not delay_running:
- _LOGGER.debug(
- "%s - Arm delay cause i'm off and event is on and no delay is running",
- self,
- )
- arm()
- return try_motion_condition
- # Ignore the event
- _LOGGER.debug("%s - Event ignored cause i'm already off", self)
- return None
- else: # I'm On
- if not event_on and not delay_running:
- _LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
- arm()
- return try_motion_condition
- if event_on and delay_running:
- _LOGGER.debug(
- "%s - Desarm off delay cause i'm on and event is on and a delay is running",
- self,
- )
- desarm()
- return None
- # Ignore the event
- _LOGGER.debug("%s - Event ignored cause i'm already on", self)
- return None
-
@callback
async def _check_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF."""
@@ -1778,17 +1363,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await under.check_initial_state(self._hvac_mode)
# Prevent from starting a VTherm if window is open
- if (
- self.is_window_auto_enabled
- and self._window_sensor_entity_id is not None
- and self._hass.states.is_state(self._window_sensor_entity_id, STATE_ON)
- and self.is_on
- and self.window_action == CONF_WINDOW_TURN_OFF
- ):
+ if self.is_on:
_LOGGER.info("%s - the window is open. Prevent starting the VTherm")
- self._window_auto_state = True
- self.save_hvac_mode()
- await self.async_set_hvac_mode(HVACMode.OFF)
+ await self._window_manager.refresh_state()
# Starts the initial control loop (don't wait for an update of temperature)
await self.async_control_heating(force=True)
@@ -1817,11 +1394,11 @@ 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._async_manage_window_auto()
+ return await self._window_manager.manage_window_auto()
except ValueError as ex:
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
@@ -1844,263 +1421,17 @@ 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)
- @callback
- async def _async_power_changed(self, event: Event[EventStateChangedData]):
- """Handle power changes."""
- _LOGGER.debug("Thermostat %s - Receive new Power event", self.name)
- _LOGGER.debug(event)
- new_state = event.data.get("new_state")
- old_state = event.data.get("old_state")
- if (
- new_state is None
- or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
- or (old_state is not None and new_state.state == old_state.state)
- ):
- return
-
- try:
- current_power = float(new_state.state)
- if math.isnan(current_power) or math.isinf(current_power):
- raise ValueError(f"Sensor has illegal state {new_state.state}")
- self._current_power = current_power
-
- if self._attr_preset_mode == PRESET_POWER:
- await self.async_control_heating()
-
- except ValueError as ex:
- _LOGGER.error("Unable to update current_power from sensor: %s", ex)
-
- @callback
- async def _async_max_power_changed(self, event: Event[EventStateChangedData]):
- """Handle power max changes."""
- _LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
- _LOGGER.debug(event)
- new_state = event.data.get("new_state")
- old_state = event.data.get("old_state")
- if (
- new_state is None
- or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
- or (old_state is not None and new_state.state == old_state.state)
- ):
- return
-
- try:
- current_power_max = float(new_state.state)
- if math.isnan(current_power_max) or math.isinf(current_power_max):
- raise ValueError(f"Sensor has illegal state {new_state.state}")
- self._current_power_max = current_power_max
- if self._attr_preset_mode == PRESET_POWER:
- await self.async_control_heating()
-
- except ValueError as ex:
- _LOGGER.error("Unable to update current_power from sensor: %s", ex)
-
- @callback
- async def _async_presence_changed(self, event: Event[EventStateChangedData]):
- """Handle presence changes."""
- new_state = event.data.get("new_state")
- _LOGGER.info(
- "%s - Presence changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
- self,
- new_state,
- self._attr_preset_mode,
- PRESET_ACTIVITY,
- )
- if new_state is None:
- return
-
- await self._async_update_presence(new_state.state)
- await self.async_control_heating(force=True)
-
- async def _async_update_presence(self, new_state: str):
- _LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
- self._presence_state = (
- STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
- )
- if self._attr_preset_mode in HIDDEN_PRESETS or self._presence_on is False:
- _LOGGER.info(
- "%s - Ignoring presence change cause in Power or Security preset or presence not configured",
- self,
- )
- return
- if new_state is None or new_state not in (
- STATE_OFF,
- STATE_ON,
- STATE_HOME,
- STATE_NOT_HOME,
- ):
- return
- if self._attr_preset_mode not in [
- PRESET_BOOST,
- PRESET_COMFORT,
- PRESET_ECO,
- PRESET_ACTIVITY,
- ]:
- return
-
- new_temp = self.find_preset_temp(self.preset_mode)
- if new_temp is not None:
- _LOGGER.debug(
- "%s - presence change in temperature mode new_temp will be: %.2f",
- self,
- new_temp,
- )
- await self._async_internal_set_temperature(new_temp)
- self.recalculate()
-
- async def _async_update_motion_temp(self):
- """Update the temperature considering the ACTIVITY preset and current motion state"""
- _LOGGER.debug(
- "%s - Calling _update_motion_temp preset_mode=%s, motion_state=%s",
- self,
- self._attr_preset_mode,
- self._motion_state,
- )
- if (
- self._motion_sensor_entity_id is None
- or self._attr_preset_mode != PRESET_ACTIVITY
- ):
- return
-
- new_preset = (
- self._motion_preset
- if self._motion_state == STATE_ON
- else self._no_motion_preset
- )
- _LOGGER.info(
- "%s - Motion condition have changes. New preset temp will be %s",
- self,
- new_preset,
- )
- # We do not change the preset which is kept to ACTIVITY but only the target_temperature
- # We take the presence into account
-
- await self._async_internal_set_temperature(
- self.find_preset_temp(new_preset)
- )
-
- _LOGGER.debug(
- "%s - regarding motion, target_temp have been set to %.2f",
- self,
- self._target_temp,
- )
-
- async def _async_underlying_entity_turn_off(self):
+ async def async_underlying_entity_turn_off(self):
"""Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off"""
for under in self._underlyings:
await under.turn_off()
- async def _async_manage_window_auto(self, in_cycle=False):
- """The management of the window auto feature"""
-
- async def dearm_window_auto(_):
- """Callback that will be called after end of WINDOW_AUTO_MAX_DURATION"""
- _LOGGER.info("Unset window auto because MAX_DURATION is exceeded")
- await deactivate_window_auto(auto=True)
-
- async def deactivate_window_auto(auto=False):
- """Deactivation of the Window auto state"""
- _LOGGER.warning(
- "%s - End auto detection of open window slope=%.3f", self, slope
- )
- # Send an event
- cause = "max duration expiration" if auto else "end of slope alert"
- self.send_event(
- EventType.WINDOW_AUTO_EVENT,
- {"type": "end", "cause": cause, "curve_slope": slope},
- )
- # Set attributes
- self._window_auto_state = False
- await self.change_window_detection_state(self._window_auto_state)
- # await self.restore_hvac_mode(True)
-
- if self._window_call_cancel:
- self._window_call_cancel()
- self._window_call_cancel = None
-
- if not self._window_auto_algo:
- return
-
- if in_cycle:
- slope = self._window_auto_algo.check_age_last_measurement(
- temperature=self._ema_temp,
- datetime_now=self.now,
- )
- else:
- slope = self._window_auto_algo.add_temp_measurement(
- temperature=self._ema_temp,
- datetime_measure=self._last_temperature_measure,
- )
-
- _LOGGER.debug(
- "%s - Window auto is on, check the alert. last slope is %.3f",
- self,
- slope if slope is not None else 0.0,
- )
-
- if self.window_bypass_state or not self.is_window_auto_enabled:
- _LOGGER.debug(
- "%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
- self,
- )
- return
-
- if (
- self._window_auto_algo.is_window_open_detected()
- and self._window_auto_state is False
- and self.hvac_mode != HVACMode.OFF
- ):
- if (
- self.proportional_algorithm
- and self.proportional_algorithm.on_percent <= 0.0
- ):
- _LOGGER.info(
- "%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
- self,
- slope,
- )
- return dearm_window_auto
-
- _LOGGER.warning(
- "%s - Start auto detection of open window slope=%.3f", self, slope
- )
-
- # Send an event
- self.send_event(
- EventType.WINDOW_AUTO_EVENT,
- {"type": "start", "cause": "slope alert", "curve_slope": slope},
- )
- # Set attributes
- self._window_auto_state = True
- await self.change_window_detection_state(self._window_auto_state)
- # self.save_hvac_mode()
- # await self.async_set_hvac_mode(HVACMode.OFF)
-
- # Arm the end trigger
- if self._window_call_cancel:
- self._window_call_cancel()
- self._window_call_cancel = None
- self._window_call_cancel = async_call_later(
- self.hass,
- timedelta(minutes=self._window_auto_max_duration),
- dearm_window_auto,
- )
-
- elif (
- self._window_auto_algo.is_window_close_detected()
- and self._window_auto_state is True
- ):
- await deactivate_window_auto(False)
-
- # For testing purpose we need to return the inner function
- return dearm_window_auto
-
def save_preset_mode(self):
"""Save the current preset mode to be restored later
We never save a hidden preset mode
@@ -2119,7 +1450,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_preset_mode not in HIDDEN_PRESETS
and self._saved_preset_mode is not None
):
- await self._async_set_preset_mode_internal(self._saved_preset_mode)
+ await self.async_set_preset_mode_internal(self._saved_preset_mode)
def save_hvac_mode(self):
"""Save the current hvac-mode to be restored later"""
@@ -2145,98 +1476,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_mode,
)
- async def check_overpowering(self) -> bool:
- """Check the overpowering condition
- Turn the preset_mode of the heater to 'power' if power conditions are exceeded
- """
+ def save_target_temp(self):
+ """Save the target temperature"""
+ self._saved_target_temp = self._target_temp
- if not self._pmax_on:
- _LOGGER.debug(
- "%s - power not configured. check_overpowering not available", self
- )
- return False
-
- if (
- self._current_power is None
- or self._device_power is None
- or self._current_power_max is None
- ):
- _LOGGER.warning(
- "%s - power not valued. check_overpowering not available", self
- )
- return False
-
- _LOGGER.debug(
- "%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
- self,
- self._current_power,
- self._current_power_max,
- self._device_power,
- )
-
- # issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
- if self.is_device_active:
- power_consumption_max = 0
- else:
- if self.is_over_climate:
- power_consumption_max = self._device_power
- else:
- power_consumption_max = max(
- self._device_power / self.nb_underlying_entities,
- self._device_power * self._prop_algorithm.on_percent,
- )
-
- ret = (self._current_power + power_consumption_max) >= self._current_power_max
- if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
- _LOGGER.warning(
- "%s - overpowering is detected. Heater preset will be set to 'power'",
- self,
- )
- if self.is_over_climate:
- self.save_hvac_mode()
- self.save_preset_mode()
- await self._async_underlying_entity_turn_off()
- await self._async_set_preset_mode_internal(PRESET_POWER)
- self.send_event(
- EventType.POWER_EVENT,
- {
- "type": "start",
- "current_power": self._current_power,
- "device_power": self._device_power,
- "current_power_max": self._current_power_max,
- "current_power_consumption": power_consumption_max,
- },
- )
-
- # Check if we need to remove the POWER preset
- if (
- self._overpowering_state
- and not ret
- and self._attr_preset_mode == PRESET_POWER
- ):
- _LOGGER.warning(
- "%s - end of overpowering is detected. Heater preset will be restored to '%s'",
- self,
- self._saved_preset_mode,
- )
- if self.is_over_climate:
- await self.restore_hvac_mode(False)
- await self.restore_preset_mode()
- self.send_event(
- EventType.POWER_EVENT,
- {
- "type": "end",
- "current_power": self._current_power,
- "device_power": self._device_power,
- "current_power_max": self._current_power_max,
- },
- )
-
- if self._overpowering_state != ret:
- self._overpowering_state = ret
- self.update_custom_attributes()
-
- return self._overpowering_state
+ async def restore_target_temp(self):
+ """Restore the saved target temp"""
+ await self.change_target_temperature(self._saved_target_temp)
async def check_central_mode(
self, new_central_mode: str | None, old_central_mode: str | None
@@ -2262,16 +1508,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.save_preset_mode()
self.save_hvac_mode()
+ is_window_detected = self._window_manager.is_window_detected
if new_central_mode == CENTRAL_MODE_AUTO:
- if self.window_state is not STATE_ON and not first_init:
+ if not is_window_detected and not first_init:
await self.restore_hvac_mode()
await self.restore_preset_mode()
- elif self.window_state is STATE_ON and self.hvac_mode == HVACMode.OFF:
+ elif is_window_detected and self.hvac_mode == HVACMode.OFF:
# do not restore but mark the reason of off with window detection
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
return
- if old_central_mode == CENTRAL_MODE_AUTO and self.window_state is not STATE_ON:
+ if old_central_mode == CENTRAL_MODE_AUTO and not is_window_detected:
save_all()
if new_central_mode == CENTRAL_MODE_STOPPED:
@@ -2320,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
@@ -2484,95 +1574,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
should have found the underlying climate to be operational"""
return True
- async def change_window_detection_state(self, new_state):
- """Change the window detection state.
- new_state is on if an open window have been detected or off else
- """
- if new_state is False:
- _LOGGER.info(
- "%s - Window is closed. Restoring hvac_mode '%s' if stopped by window detection or temperature %s",
- self,
- self._saved_hvac_mode,
- self._saved_target_temp,
- )
- if self._window_action in [CONF_WINDOW_FROST_TEMP, CONF_WINDOW_ECO_TEMP]:
- await self._async_internal_set_temperature(self._saved_target_temp)
-
- # default to TURN_OFF
- elif self._window_action in [CONF_WINDOW_TURN_OFF]:
- if (
- self.last_central_mode != CENTRAL_MODE_STOPPED
- and self.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
- ):
- self.set_hvac_off_reason(None)
- await self.restore_hvac_mode(True)
- elif self._window_action in [CONF_WINDOW_FAN_ONLY]:
- if self.last_central_mode != CENTRAL_MODE_STOPPED:
- self.set_hvac_off_reason(None)
- await self.restore_hvac_mode(True)
- else:
- _LOGGER.error(
- "%s - undefined window_action %s. Please open a bug in the github of this project with this log",
- self,
- self._window_action,
- )
- else:
- _LOGGER.info(
- "%s - Window is open. Apply window action %s", self, self._window_action
- )
- if self._window_action == CONF_WINDOW_TURN_OFF and not self.is_on:
- _LOGGER.debug(
- "%s is already off. Forget turning off VTherm due to window detection"
- )
- return
-
- if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
- if self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
- self.save_hvac_mode()
- elif self._window_action in [
- CONF_WINDOW_FROST_TEMP,
- CONF_WINDOW_ECO_TEMP,
- ]:
- self._saved_target_temp = self._target_temp
-
- if (
- self._window_action == CONF_WINDOW_FAN_ONLY
- and HVACMode.FAN_ONLY in self.hvac_modes
- ):
- await self.async_set_hvac_mode(HVACMode.FAN_ONLY)
- elif (
- self._window_action == CONF_WINDOW_FROST_TEMP
- and self._presets.get(PRESET_FROST_PROTECTION) is not None
- ):
- await self._async_internal_set_temperature(
- self.find_preset_temp(PRESET_FROST_PROTECTION)
- )
- elif (
- self._window_action == CONF_WINDOW_ECO_TEMP
- and self._presets.get(PRESET_ECO) is not None
- ):
- await self._async_internal_set_temperature(
- self.find_preset_temp(PRESET_ECO)
- )
- else: # default is to turn_off
- self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
- await self.async_set_hvac_mode(HVACMode.OFF)
-
async def async_control_heating(self, force=False, _=None) -> bool:
"""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,
)
# check auto_window conditions
- await self._async_manage_window_auto(in_cycle=True)
+ await self._window_manager.manage_window_auto(in_cycle=True)
- # Issue 56 in over_climate mode, if the underlying climate is not initialized, try to initialize it
+ # In over_climate mode, if the underlying climate is not initialized, try to initialize it
if not self.is_initialized:
if not self.init_underlyings():
# still not found, we an stop here
@@ -2580,14 +1596,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Check overpowering condition
# Not necessary for switch because each switch is checking at startup
- overpowering: bool = await self.check_overpowering()
- if overpowering:
+ overpowering = await self._power_manager.check_overpowering()
+ if overpowering == STATE_ON:
_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
@@ -2595,7 +1611,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", self)
# A security to force stop heater if still active
if self.is_device_active:
- await self._async_underlying_entity_turn_off()
+ await self.async_underlying_entity_turn_off()
return True
for under in self._underlyings:
@@ -2650,45 +1666,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"comfort_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_COMFORT), 0
),
- "power_temp": self._power_temp,
"target_temperature_step": self.target_temperature_step,
"ext_current_temperature": self._cur_ext_temp,
"ac_mode": self._ac_mode,
- "current_power": self._current_power,
- "current_power_max": self._current_power_max,
"saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode,
- "motion_sensor_entity_id": self._motion_sensor_entity_id,
- "motion_state": self._motion_state,
- "power_sensor_entity_id": self._power_sensor_entity_id,
- "max_power_sensor_entity_id": self._max_power_sensor_entity_id,
- "overpowering_state": self.overpowering_state,
- "presence_sensor_entity_id": self._presence_sensor_entity_id,
- "presence_state": self._presence_state,
- "window_state": self.window_state,
- "window_auto_state": self.window_auto_state,
- "window_bypass_state": self._window_bypass_state,
- "window_sensor_entity_id": self._window_sensor_entity_id,
- "window_delay_sec": self._window_delay_sec,
- "window_auto_enabled": self.is_window_auto_enabled,
- "window_auto_open_threshold": self._window_auto_open_threshold,
- "window_auto_close_threshold": self._window_auto_close_threshold,
- "window_auto_max_duration": self._window_auto_max_duration,
- "window_action": self.window_action,
- "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,
- "device_power": self._device_power,
- ATTR_MEAN_POWER_CYCLE: self.mean_cycle_power,
ATTR_TOTAL_ENERGY: self.total_energy,
"last_update_datetime": self.now.isoformat(),
"timezone": str(self._current_tz),
@@ -2711,20 +1701,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
),
}
- _LOGGER.debug(
- "%s - update_custom_attributes saved energy is %s",
- self,
- self.total_energy,
- )
+ for manager in self._managers:
+ manager.add_custom_attributes(self._attr_extra_state_attributes)
@overrides
def async_write_ha_state(self):
"""overrides to have log"""
- _LOGGER.debug(
- "%s - async_write_ha_state written state energy is %s",
- self,
- self._total_energy,
- )
return super().async_write_ha_state()
@property
@@ -2732,6 +1714,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""True if the Thermostat is regulated by valve"""
return False
+ @property
+ def saved_target_temp(self) -> float:
+ """Returns the saved_target_temp"""
+ return self._saved_target_temp
+
+ @property
+ def saved_hvac_mode(self) -> float:
+ """Returns the saved_hvac_mode"""
+ return self._saved_hvac_mode
+
+ @property
+ def saved_preset_mode(self) -> float:
+ """Returns the saved_preset_mode"""
+ return self._saved_preset_mode
+
@callback
def async_registry_entry_updated(self):
"""update the entity if the config entry have been updated
@@ -2748,7 +1745,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entity_id: climate.thermostat_1
"""
_LOGGER.info("%s - Calling service_set_presence, presence: %s", self, presence)
- await self._async_update_presence(presence)
+ await self._presence_manager.update_presence(presence)
await self.async_control_heating(force=True)
async def service_set_preset_temperature(
@@ -2776,7 +1773,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if preset in self._presets:
if temperature is not None:
self._presets[preset] = temperature
- if self._presence_on and temperature_away is not None:
+ if self._presence_manager.is_configured and temperature_away is not None:
self._presets_away[self.get_preset_away_name(preset)] = temperature_away
else:
_LOGGER.warning(
@@ -2788,19 +1785,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# If the changed preset is active, change the current temperature
# Issue #119 - reload new preset temperature also in ac mode
if preset.startswith(self._attr_preset_mode):
- await self._async_set_preset_mode_internal(
+ await self.async_set_preset_mode_internal(
preset.rstrip(PRESET_AC_SUFFIX), force=True
)
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
@@ -2809,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()
@@ -2841,22 +1840,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self,
window_bypass,
)
- self._window_bypass_state = window_bypass
- if not self._window_bypass_state and self._window_state:
- _LOGGER.info(
- "%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
- self,
- HVACMode.OFF,
- )
- self.save_hvac_mode()
- await self.async_set_hvac_mode(HVACMode.OFF)
- if self._window_bypass_state and self._window_state:
- _LOGGER.info(
- "%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
- self,
- )
- await self.restore_hvac_mode(True)
- self.update_custom_attributes()
+ if await self._window_manager.set_window_bypass(window_bypass):
+ self.update_custom_attributes()
def send_event(self, event_type: EventType, data: dict):
"""Send an event"""
@@ -2899,7 +1884,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
CONF_USE_PRESETS_CENTRAL_CONFIG,
)
- if self._entry_infos.get(CONF_USE_PRESENCE_FEATURE) is True:
+ # refacto
+ # if self._entry_infos.get(CONF_USE_PRESENCE_FEATURE) is True:
+ if self._presence_manager.is_configured:
presets_away = calculate_presets(
(
CONF_PRESETS_AWAY_WITH_AC.items()
@@ -2919,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(
@@ -2928,12 +1916,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
else:
_LOGGER.debug("No preset_modes")
- if self._motion_on:
+ if self._motion_manager.is_configured:
self._attr_preset_modes.append(PRESET_ACTIVITY)
# Re-applicate the last preset if any to take change into account
if self._attr_preset_mode:
- await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
+ await self.async_set_preset_mode_internal(self._attr_preset_mode, True)
async def async_turn_off(self) -> None:
await self.async_set_hvac_mode(HVACMode.OFF)
@@ -2943,3 +1931,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self.async_set_hvac_mode(HVACMode.COOL)
else:
await self.async_set_hvac_mode(HVACMode.HEAT)
+
+ def is_preset_configured(self, preset) -> bool:
+ """Returns True if the preset in argument is configured"""
+ return self._presets.get(preset, None) is not None
diff --git a/custom_components/versatile_thermostat/binary_sensor.py b/custom_components/versatile_thermostat/binary_sensor.py
index 36a1e06..c9b4bc7 100644
--- a/custom_components/versatile_thermostat/binary_sensor.py
+++ b/custom_components/versatile_thermostat/binary_sensor.py
@@ -25,10 +25,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .vtherm_api import VersatileThermostatAPI
-from .commons import (
- VersatileThermostatBaseEntity,
- check_and_extract_service_configuration,
-)
+from .commons import check_and_extract_service_configuration
+from .base_entity import VersatileThermostatBaseEntity
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
@@ -111,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
@@ -150,7 +148,7 @@ class OverpoweringBinarySensor(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.overpowering_state is True
+ self._attr_is_on = self.my_climate.overpowering_state is STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@@ -319,8 +317,8 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
"""Called when my climate have change"""
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
- if self.my_climate.window_bypass_state in [True, False]:
- self._attr_is_on = self.my_climate.window_bypass_state
+ if self.my_climate.is_window_bypass in [True, False]:
+ self._attr_is_on = self.my_climate.is_window_bypass
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py
index 4fc586e..61334d8 100644
--- a/custom_components/versatile_thermostat/climate.py
+++ b/custom_components/versatile_thermostat/climate.py
@@ -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(
diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py
index 3e768ed..6018d01 100644
--- a/custom_components/versatile_thermostat/commons.py
+++ b/custom_components/versatile_thermostat/commons.py
@@ -3,17 +3,14 @@
# pylint: disable=line-too-long
import logging
-from datetime import timedelta
-from homeassistant.core import HomeAssistant, callback, Event
-from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
-from homeassistant.helpers.entity_component import EntityComponent
-from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
-from homeassistant.helpers.event import async_track_state_change_event, async_call_later
+from types import MappingProxyType
+from typing import Any, TypeVar
+from .const import ServiceConfigurationError
+from .underlyings import UnderlyingEntity
-from .base_thermostat import BaseThermostat
-from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
+ConfigData = MappingProxyType[str, Any]
+T = TypeVar("T", bound=UnderlyingEntity)
_LOGGER = logging.getLogger(__name__)
@@ -135,104 +132,3 @@ def check_and_extract_service_configuration(service_config) -> dict:
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
)
return ret
-
-
-class VersatileThermostatBaseEntity(Entity):
- """A base class for all entities"""
-
- _my_climate: BaseThermostat
- hass: HomeAssistant
- _config_id: str
- _device_name: str
-
- def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
- """The CTOR"""
- self.hass = hass
- self._config_id = config_id
- self._device_name = device_name
- self._my_climate = None
- self._cancel_call = None
- self._attr_has_entity_name = True
-
- @property
- def should_poll(self) -> bool:
- """Do not poll for those entities"""
- return False
-
- @property
- def my_climate(self) -> BaseThermostat | None:
- """Returns my climate if found"""
- if not self._my_climate:
- self._my_climate = self.find_my_versatile_thermostat()
- if self._my_climate:
- # Only the first time
- self.my_climate_is_initialized()
- return self._my_climate
-
- @property
- def device_info(self) -> DeviceInfo:
- """Return the device info."""
- return DeviceInfo(
- entry_type=DeviceEntryType.SERVICE,
- identifiers={(DOMAIN, self._config_id)},
- name=self._device_name,
- manufacturer=DEVICE_MANUFACTURER,
- model=DOMAIN,
- )
-
- def find_my_versatile_thermostat(self) -> BaseThermostat:
- """Find the underlying climate entity"""
- try:
- component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
- for entity in component.entities:
- # _LOGGER.debug("Device_info is %s", entity.device_info)
- if entity.device_info == self.device_info:
- _LOGGER.debug("Found %s!", entity)
- return entity
- except KeyError:
- pass
-
- return None
-
- @callback
- async def async_added_to_hass(self):
- """Listen to my climate state change"""
-
- # Check delay condition
- async def try_find_climate(_):
- _LOGGER.debug(
- "%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
- )
- mcl = self.my_climate
- if mcl:
- if self._cancel_call:
- self._cancel_call()
- self._cancel_call = None
- self.async_on_remove(
- async_track_state_change_event(
- self.hass,
- [mcl.entity_id],
- self.async_my_climate_changed,
- )
- )
- else:
- _LOGGER.debug("%s - no entity to listen. Try later", self)
- self._cancel_call = async_call_later(
- self.hass, timedelta(seconds=1), try_find_climate
- )
-
- await try_find_climate(None)
-
- @callback
- def my_climate_is_initialized(self):
- """Called when the associated climate is initialized"""
- return
-
- @callback
- async def async_my_climate_changed(
- self, event: Event
- ): # pylint: disable=unused-argument
- """Called when my climate have change
- This method aims to be overriden to take the status change
- """
- return
diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py
index 7e824a9..9190647 100644
--- a/custom_components/versatile_thermostat/config_flow.py
+++ b/custom_components/versatile_thermostat/config_flow.py
@@ -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
diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py
index 92fba1f..90e4121 100644
--- a/custom_components/versatile_thermostat/config_schema.py
+++ b/custom_components/versatile_thermostat/config_schema.py
@@ -368,14 +368,14 @@ 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,
- default=DEFAULT_SECURITY_MIN_ON_PERCENT,
+ CONF_SAFETY_MIN_ON_PERCENT,
+ default=DEFAULT_SAFETY_MIN_ON_PERCENT,
): vol.Coerce(float),
vol.Required(
- CONF_SECURITY_DEFAULT_ON_PERCENT,
- default=DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
+ CONF_SAFETY_DEFAULT_ON_PERCENT,
+ default=DEFAULT_SAFETY_DEFAULT_ON_PERCENT,
): vol.Coerce(float),
}
)
diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py
index 52cfecf..9f14082 100644
--- a/custom_components/versatile_thermostat/const.py
+++ b/custom_components/versatile_thermostat/const.py
@@ -4,6 +4,7 @@
import logging
import math
from typing import Literal
+
from datetime import datetime
from enum import Enum
@@ -28,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"
@@ -41,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"
@@ -83,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"
@@ -285,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,
@@ -373,13 +374,13 @@ 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"
-DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
-DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
+DEFAULT_SAFETY_MIN_ON_PERCENT = 0.5
+DEFAULT_SAFETY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
diff --git a/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py b/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py
new file mode 100644
index 0000000..3880b36
--- /dev/null
+++ b/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py
@@ -0,0 +1,236 @@
+""" Implements the Auto-start/stop Feature Manager """
+
+# pylint: disable=line-too-long
+
+import logging
+from typing import Any
+
+from homeassistant.core import (
+ HomeAssistant,
+)
+from homeassistant.components.climate import HVACMode
+
+from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData
+
+from .base_manager import BaseFeatureManager
+
+from .auto_start_stop_algorithm import (
+ AutoStartStopDetectionAlgorithm,
+ AUTO_START_STOP_ACTION_OFF,
+ AUTO_START_STOP_ACTION_ON,
+)
+
+
+_LOGGER = logging.getLogger(__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: TYPE_AUTO_START_STOP_LEVELS = (
+ 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
+
+ @overrides
+ def post_init(self, entry_infos: ConfigData):
+ """Reinit of the manager"""
+
+ 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
+
+ # Instanciate the auto start stop algo
+ self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
+ self._auto_start_stop_level, self.name
+ )
+
+ @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 auto-start-stop and an eventual action
+ Return False if we should stop the control_heating method"""
+
+ 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)
+ return True
+
+ slope = (
+ self._vtherm.last_temperature_slope or 0
+ ) / 60 # to have the slope in °/min
+ action = self._auto_start_stop_algo.calculate_action(
+ self._vtherm.hvac_mode,
+ self._vtherm.saved_hvac_mode,
+ self._vtherm.target_temperature,
+ self._vtherm.current_temperature,
+ slope,
+ self._vtherm.now,
+ )
+ _LOGGER.debug("%s - auto_start_stop action is %s", self, action)
+ if action == AUTO_START_STOP_ACTION_OFF and self._vtherm.is_on:
+ _LOGGER.info(
+ "%s - Turning OFF the Vtherm 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
+ self._vtherm.send_event(
+ event_type=EventType.AUTO_START_STOP_EVENT,
+ data={
+ "type": "stop",
+ "name": self.name,
+ "cause": "Auto stop conditions reached",
+ "hvac_mode": self._vtherm.hvac_mode,
+ "saved_hvac_mode": self._vtherm.saved_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,
+ },
+ )
+
+ # Stop here
+ return False
+ elif (
+ action == AUTO_START_STOP_ACTION_ON
+ and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
+ ):
+ _LOGGER.info(
+ "%s - Turning ON the Vtherm due to auto-start-stop conditions", self
+ )
+ await self._vtherm.async_turn_on()
+
+ # Send an event
+ self._vtherm.send_event(
+ event_type=EventType.AUTO_START_STOP_EVENT,
+ data={
+ "type": "start",
+ "name": self.name,
+ "cause": "Auto start conditions reached",
+ "hvac_mode": self._vtherm.hvac_mode,
+ "saved_hvac_mode": self._vtherm.saved_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._vtherm.update_custom_attributes()
+
+ return True
+
+ def set_auto_start_stop_enable(self, is_enabled: bool):
+ """Enable/Disable the auto-start/stop feature"""
+ self._is_auto_start_stop_enabled = is_enabled
+ if (
+ self._vtherm.hvac_mode == HVACMode.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"
+ )
+ self.hass.create_task(self._vtherm.async_turn_on())
+
+ # 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": self._vtherm.hvac_mode,
+ "saved_hvac_mode": self._vtherm.saved_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,
+ },
+ )
+
+ self._vtherm.update_custom_attributes()
+
+ 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_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,
+ }
+ )
+
+ @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) -> TYPE_AUTO_START_STOP_LEVELS:
+ """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 == HVACMode.OFF
+ and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
+ )
+
+ def __str__(self):
+ return f"AutoStartStopManager-{self.name}"
diff --git a/custom_components/versatile_thermostat/feature_motion_manager.py b/custom_components/versatile_thermostat/feature_motion_manager.py
new file mode 100644
index 0000000..6232386
--- /dev/null
+++ b/custom_components/versatile_thermostat/feature_motion_manager.py
@@ -0,0 +1,343 @@
+""" Implements the Motion Feature Manager """
+
+# pylint: disable=line-too-long
+
+import logging
+from typing import Any
+from datetime import timedelta
+
+from homeassistant.const import (
+ STATE_ON,
+ STATE_OFF,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import (
+ HomeAssistant,
+ callback,
+ Event,
+)
+from homeassistant.helpers.event import (
+ async_track_state_change_event,
+ EventStateChangedData,
+ async_call_later,
+)
+
+from homeassistant.components.climate import (
+ PRESET_ACTIVITY,
+)
+
+from homeassistant.exceptions import ConditionError
+from homeassistant.helpers import condition
+
+from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData
+
+from .base_manager import BaseFeatureManager
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class FeatureMotionManager(BaseFeatureManager):
+ """The implementation of the Motion feature"""
+
+ unrecorded_attributes = frozenset(
+ {
+ "motion_sensor_entity_id",
+ "is_motion_configured",
+ "motion_delay_sec",
+ "motion_off_delay_sec",
+ "motion_preset",
+ "no_motion_preset",
+ }
+ )
+
+ def __init__(self, vtherm: Any, hass: HomeAssistant):
+ """Init of a featureManager"""
+ super().__init__(vtherm, hass)
+ self._motion_state: str = STATE_UNAVAILABLE
+ self._motion_sensor_entity_id: str = None
+ self._motion_delay_sec: int | None = 0
+ self._motion_off_delay_sec: int | None = 0
+ self._motion_preset: str | None = None
+ self._no_motion_preset: str | None = None
+ self._is_configured: bool = False
+ self._motion_call_cancel: callable = None
+
+ @overrides
+ def post_init(self, entry_infos: ConfigData):
+ """Reinit of the manager"""
+ self.dearm_motion_timer()
+
+ self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR, None)
+ self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY, 0)
+ self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY, None)
+ if not self._motion_off_delay_sec:
+ self._motion_off_delay_sec = self._motion_delay_sec
+
+ self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
+ self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
+ if (
+ self._motion_sensor_entity_id is not None
+ and self._motion_preset is not None
+ and self._no_motion_preset is not None
+ ):
+ self._is_configured = True
+ self._motion_state = STATE_UNKNOWN
+
+ @overrides
+ def start_listening(self):
+ """Start listening the underlying entity"""
+ if self._is_configured:
+ self.stop_listening()
+ self.add_listener(
+ async_track_state_change_event(
+ self.hass,
+ [self._motion_sensor_entity_id],
+ self._motion_sensor_changed,
+ )
+ )
+
+ @overrides
+ def stop_listening(self):
+ """Stop listening and remove the eventual timer still running"""
+ self.dearm_motion_timer()
+ super().stop_listening()
+
+ def dearm_motion_timer(self):
+ """Dearm the eventual motion time running"""
+ if self._motion_call_cancel:
+ self._motion_call_cancel()
+ self._motion_call_cancel = None
+
+ @overrides
+ async def refresh_state(self) -> bool:
+ """Tries to get the last state from sensor
+ Returns True if a change has been made"""
+ ret = False
+ if self._is_configured:
+
+ motion_state = self.hass.states.get(self._motion_sensor_entity_id)
+ if motion_state and motion_state.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ _LOGGER.debug(
+ "%s - Motion state have been retrieved: %s",
+ self,
+ self._motion_state,
+ )
+ # recalculate the right target_temp in activity mode
+ ret = await self.update_motion_state(motion_state.state, False)
+
+ return ret
+
+ @callback
+ async def _motion_sensor_changed(self, event: Event[EventStateChangedData]):
+ """Handle motion sensor changes."""
+ new_state = event.data.get("new_state")
+ _LOGGER.info(
+ "%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
+ self,
+ new_state,
+ self._vtherm.preset_mode,
+ PRESET_ACTIVITY,
+ )
+
+ if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
+ return
+
+ # Check delay condition
+ async def try_motion_condition(_):
+ self.dearm_motion_timer()
+
+ try:
+ delay = (
+ self._motion_delay_sec
+ if new_state.state == STATE_ON
+ else self._motion_off_delay_sec
+ )
+ long_enough = condition.state(
+ self.hass,
+ self._motion_sensor_entity_id,
+ new_state.state,
+ timedelta(seconds=delay),
+ )
+ except ConditionError:
+ long_enough = False
+
+ if not long_enough:
+ _LOGGER.debug(
+ "Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
+ )
+ # Get sensor current state
+ motion_state = self.hass.states.get(self._motion_sensor_entity_id)
+ _LOGGER.debug(
+ "%s - motion_state=%s, new_state.state=%s",
+ self,
+ motion_state.state,
+ new_state.state,
+ )
+ if (
+ motion_state.state == new_state.state
+ and new_state.state == STATE_ON
+ ):
+ _LOGGER.debug(
+ "%s - the motion sensor is finally 'on' after the delay", self
+ )
+ long_enough = True
+ else:
+ long_enough = False
+
+ if long_enough:
+ _LOGGER.debug("%s - Motion delay condition is satisfied", self)
+ await self.update_motion_state(new_state.state)
+ else:
+ await self.update_motion_state(
+ STATE_ON if new_state.state == STATE_OFF else STATE_OFF
+ )
+
+ im_on = self._motion_state == STATE_ON
+ delay_running = self._motion_call_cancel is not None
+ event_on = new_state.state == STATE_ON
+
+ def arm():
+ """Arm the timer"""
+ delay = (
+ self._motion_delay_sec
+ if new_state.state == STATE_ON
+ else self._motion_off_delay_sec
+ )
+ self._motion_call_cancel = async_call_later(
+ self.hass, timedelta(seconds=delay), try_motion_condition
+ )
+
+ # if I'm off
+ if not im_on:
+ if event_on and not delay_running:
+ _LOGGER.debug(
+ "%s - Arm delay cause i'm off and event is on and no delay is running",
+ self,
+ )
+ arm()
+ return try_motion_condition
+ # Ignore the event
+ _LOGGER.debug("%s - Event ignored cause i'm already off", self)
+ return None
+ else: # I'm On
+ if not event_on and not delay_running:
+ _LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
+ arm()
+ return try_motion_condition
+ if event_on and delay_running:
+ _LOGGER.debug(
+ "%s - Desarm off delay cause i'm on and event is on and a delay is running",
+ self,
+ )
+ self.dearm_motion_timer()
+ return None
+ # Ignore the event
+ _LOGGER.debug("%s - Event ignored cause i'm already on", self)
+ return None
+
+ async def update_motion_state(
+ self, new_state: str = None, recalculate: bool = True
+ ) -> bool:
+ """Update the value of the motion sensor and update the VTherm state accordingly
+ Return true if a change has been made"""
+
+ _LOGGER.info("%s - Updating motion state. New state is %s", self, new_state)
+ old_motion_state = self._motion_state
+ if new_state is not None:
+ self._motion_state = STATE_ON if new_state == STATE_ON else STATE_OFF
+
+ if self._vtherm.preset_mode == PRESET_ACTIVITY:
+ new_preset = self.get_current_motion_preset()
+ _LOGGER.info(
+ "%s - Motion condition have changes. New preset temp will be %s",
+ self,
+ new_preset,
+ )
+ # We do not change the preset which is kept to ACTIVITY but only the target_temperature
+ # We take the motion into account
+ new_temp = self._vtherm.find_preset_temp(new_preset)
+ old_temp = self._vtherm.target_temperature
+ if new_temp != old_temp:
+ await self._vtherm.change_target_temperature(new_temp)
+
+ if new_temp != old_temp and recalculate:
+ self._vtherm.recalculate()
+ await self._vtherm.async_control_heating(force=True)
+
+ return old_motion_state != self._motion_state
+
+ def get_current_motion_preset(self) -> str:
+ """Calculate and return the current motion preset"""
+ return (
+ self._motion_preset
+ if self._motion_state == STATE_ON
+ else self._no_motion_preset
+ )
+
+ def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
+ """Add some custom attributes"""
+ extra_state_attributes.update(
+ {
+ "motion_sensor_entity_id": self._motion_sensor_entity_id,
+ "motion_state": self._motion_state,
+ "is_motion_configured": self._is_configured,
+ "motion_delay_sec": self._motion_delay_sec,
+ "motion_off_delay_sec": self._motion_off_delay_sec,
+ "motion_preset": self._motion_preset,
+ "no_motion_preset": self._no_motion_preset,
+ }
+ )
+
+ @overrides
+ @property
+ def is_configured(self) -> bool:
+ """Return True of the motion is configured"""
+ return self._is_configured
+
+ @property
+ def motion_state(self) -> str | None:
+ """Return the current motion state STATE_ON or STATE_OFF
+ or STATE_UNAVAILABLE if not configured"""
+ if not self._is_configured:
+ return STATE_UNAVAILABLE
+ return self._motion_state
+
+ @property
+ def is_motion_detected(self) -> bool:
+ """Return true if the motion is configured and motion sensor is OFF"""
+ return self._is_configured and self._motion_state in [
+ STATE_ON,
+ ]
+
+ @property
+ def motion_sensor_entity_id(self) -> bool:
+ """Return true if the motion is configured and motion sensor is OFF"""
+ return self._motion_sensor_entity_id
+
+ @property
+ def motion_delay_sec(self) -> bool:
+ """Return the motion delay"""
+ return self._motion_delay_sec
+
+ @property
+ def motion_off_delay_sec(self) -> bool:
+ """Return motion delay off"""
+ return self._motion_off_delay_sec
+
+ @property
+ def motion_preset(self) -> bool:
+ """Return motion preset"""
+ return self._motion_preset
+
+ @property
+ def no_motion_preset(self) -> bool:
+ """Return no motion preset"""
+ return self._no_motion_preset
+
+ def __str__(self):
+ return f"MotionManager-{self.name}"
diff --git a/custom_components/versatile_thermostat/feature_power_manager.py b/custom_components/versatile_thermostat/feature_power_manager.py
new file mode 100644
index 0000000..7af6bc9
--- /dev/null
+++ b/custom_components/versatile_thermostat/feature_power_manager.py
@@ -0,0 +1,368 @@
+""" Implements the Power Feature Manager """
+
+# pylint: disable=line-too-long
+
+import logging
+from typing import Any
+
+from homeassistant.const import (
+ STATE_ON,
+ STATE_OFF,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+
+
+from homeassistant.core import (
+ HomeAssistant,
+ callback,
+ Event,
+)
+from homeassistant.helpers.event import (
+ async_track_state_change_event,
+ EventStateChangedData,
+)
+from homeassistant.components.climate import HVACMode
+
+from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData
+
+from .base_manager import BaseFeatureManager
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class FeaturePowerManager(BaseFeatureManager):
+ """The implementation of the Power feature"""
+
+ unrecorded_attributes = frozenset(
+ {
+ "power_sensor_entity_id",
+ "max_power_sensor_entity_id",
+ "is_power_configured",
+ "device_power",
+ "power_temp",
+ "current_power",
+ "current_max_power",
+ }
+ )
+
+ def __init__(self, vtherm: Any, hass: HomeAssistant):
+ """Init of a featureManager"""
+ super().__init__(vtherm, hass)
+ self._power_sensor_entity_id = None
+ self._max_power_sensor_entity_id = None
+ self._current_power = None
+ self._current_max_power = None
+ self._power_temp = None
+ self._overpowering_state = STATE_UNAVAILABLE
+ self._is_configured: bool = False
+ self._device_power: float = 0
+
+ @overrides
+ def post_init(self, entry_infos: ConfigData):
+ """Reinit of the manager"""
+
+ # Power management
+ self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
+ self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
+ self._power_temp = entry_infos.get(CONF_PRESET_POWER)
+
+ self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
+ self._is_configured = False
+ self._current_power = None
+ self._current_max_power = None
+ if (
+ entry_infos.get(CONF_USE_POWER_FEATURE, False)
+ and self._max_power_sensor_entity_id
+ and self._power_sensor_entity_id
+ and self._device_power
+ ):
+ self._is_configured = True
+ self._overpowering_state = STATE_UNKNOWN
+ else:
+ _LOGGER.info("%s - Power management is not fully configured", self)
+
+ @overrides
+ def start_listening(self):
+ """Start listening the underlying entity"""
+ if self._is_configured:
+ self.stop_listening()
+ else:
+ return
+
+ self.add_listener(
+ async_track_state_change_event(
+ self.hass,
+ [self._power_sensor_entity_id],
+ self._async_power_sensor_changed,
+ )
+ )
+
+ self.add_listener(
+ async_track_state_change_event(
+ self.hass,
+ [self._max_power_sensor_entity_id],
+ self._async_max_power_sensor_changed,
+ )
+ )
+
+ @overrides
+ async def refresh_state(self) -> bool:
+ """Tries to get the last state from sensor
+ Returns True if a change has been made"""
+ ret = False
+ if self._is_configured:
+ # try to acquire current power and power max
+ current_power_state = self.hass.states.get(self._power_sensor_entity_id)
+ if current_power_state and current_power_state.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ self._current_power = float(current_power_state.state)
+ _LOGGER.debug(
+ "%s - Current power have been retrieved: %.3f",
+ self,
+ self._current_power,
+ )
+ ret = True
+
+ # Try to acquire power max
+ current_power_max_state = self.hass.states.get(
+ self._max_power_sensor_entity_id
+ )
+ if current_power_max_state and current_power_max_state.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ self._current_max_power = float(current_power_max_state.state)
+ _LOGGER.debug(
+ "%s - Current power max have been retrieved: %.3f",
+ self,
+ self._current_max_power,
+ )
+ ret = True
+
+ return ret
+
+ @callback
+ async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]):
+ """Handle power changes."""
+ _LOGGER.debug("Thermostat %s - Receive new Power event", self)
+ _LOGGER.debug(event)
+ new_state = event.data.get("new_state")
+ old_state = event.data.get("old_state")
+ if (
+ new_state is None
+ or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
+ or (old_state is not None and new_state.state == old_state.state)
+ ):
+ return
+
+ try:
+ current_power = float(new_state.state)
+ if math.isnan(current_power) or math.isinf(current_power):
+ raise ValueError(f"Sensor has illegal state {new_state.state}")
+ self._current_power = current_power
+
+ if self._vtherm.preset_mode == PRESET_POWER:
+ await self._vtherm.async_control_heating()
+
+ except ValueError as ex:
+ _LOGGER.error("Unable to update current_power from sensor: %s", ex)
+
+ @callback
+ async def _async_max_power_sensor_changed(
+ self, event: Event[EventStateChangedData]
+ ):
+ """Handle power max changes."""
+ _LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
+ _LOGGER.debug(event)
+ new_state = event.data.get("new_state")
+ old_state = event.data.get("old_state")
+ if (
+ new_state is None
+ or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
+ or (old_state is not None and new_state.state == old_state.state)
+ ):
+ return
+
+ try:
+ current_power_max = float(new_state.state)
+ if math.isnan(current_power_max) or math.isinf(current_power_max):
+ raise ValueError(f"Sensor has illegal state {new_state.state}")
+ self._current_max_power = current_power_max
+ if self._vtherm.preset_mode == PRESET_POWER:
+ await self._vtherm.async_control_heating()
+
+ except ValueError as ex:
+ _LOGGER.error("Unable to update current_power from sensor: %s", ex)
+
+ def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
+ """Add some custom attributes"""
+ extra_state_attributes.update(
+ {
+ "power_sensor_entity_id": self._power_sensor_entity_id,
+ "max_power_sensor_entity_id": self._max_power_sensor_entity_id,
+ "overpowering_state": self._overpowering_state,
+ "is_power_configured": self._is_configured,
+ "device_power": self._device_power,
+ "power_temp": self._power_temp,
+ "current_power": self._current_power,
+ "current_max_power": self._current_max_power,
+ "mean_cycle_power": self.mean_cycle_power,
+ }
+ )
+
+ async def check_overpowering(self) -> bool:
+ """Check the overpowering condition
+ Turn the preset_mode of the heater to 'power' if power conditions are exceeded
+ Returns True if overpowering is 'on'
+ """
+
+ if not self._is_configured:
+ return False
+
+ if (
+ self._current_power is None
+ or self._device_power is None
+ or self._current_max_power is None
+ ):
+ _LOGGER.warning(
+ "%s - power not valued. check_overpowering not available", self
+ )
+ return False
+
+ _LOGGER.debug(
+ "%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
+ self,
+ self._current_power,
+ self._current_max_power,
+ self._device_power,
+ )
+
+ # issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
+ if self._vtherm.is_device_active:
+ power_consumption_max = 0
+ else:
+ if self._vtherm.is_over_climate:
+ power_consumption_max = self._device_power
+ else:
+ power_consumption_max = max(
+ self._device_power / self._vtherm.nb_underlying_entities,
+ self._device_power * self._vtherm.proportional_algorithm.on_percent,
+ )
+
+ ret = (self._current_power + power_consumption_max) >= self._current_max_power
+ if (
+ self._overpowering_state == STATE_OFF
+ and ret
+ and self._vtherm.hvac_mode != HVACMode.OFF
+ ):
+ _LOGGER.warning(
+ "%s - overpowering is detected. Heater preset will be set to 'power'",
+ self,
+ )
+ if self._vtherm.is_over_climate:
+ self._vtherm.save_hvac_mode()
+ self._vtherm.save_preset_mode()
+ await self._vtherm.async_underlying_entity_turn_off()
+ await self._vtherm.async_set_preset_mode_internal(PRESET_POWER)
+ self._vtherm.send_event(
+ EventType.POWER_EVENT,
+ {
+ "type": "start",
+ "current_power": self._current_power,
+ "device_power": self._device_power,
+ "current_max_power": self._current_max_power,
+ "current_power_consumption": power_consumption_max,
+ },
+ )
+
+ # Check if we need to remove the POWER preset
+ if (
+ self._overpowering_state == STATE_ON
+ and not ret
+ and self._vtherm.preset_mode == PRESET_POWER
+ ):
+ _LOGGER.warning(
+ "%s - end of overpowering is detected. Heater preset will be restored to '%s'",
+ self,
+ self._vtherm._saved_preset_mode, # pylint: disable=protected-access
+ )
+ if self._vtherm.is_over_climate:
+ await self._vtherm.restore_hvac_mode(False)
+ await self._vtherm.restore_preset_mode()
+ self._vtherm.send_event(
+ EventType.POWER_EVENT,
+ {
+ "type": "end",
+ "current_power": self._current_power,
+ "device_power": self._device_power,
+ "current_max_power": self._current_max_power,
+ },
+ )
+
+ new_overpowering_state = STATE_ON if ret else STATE_OFF
+ if self._overpowering_state != new_overpowering_state:
+ self._overpowering_state = new_overpowering_state
+ self._vtherm.update_custom_attributes()
+
+ return self._overpowering_state == STATE_ON
+
+ @overrides
+ @property
+ def is_configured(self) -> bool:
+ """Return True of the presence is configured"""
+ return self._is_configured
+
+ @property
+ def overpowering_state(self) -> str | None:
+ """Return the current overpowering state STATE_ON or STATE_OFF
+ or STATE_UNAVAILABLE if not configured"""
+ if not self._is_configured:
+ return STATE_UNAVAILABLE
+ return self._overpowering_state
+
+ @property
+ def max_power_sensor_entity_id(self) -> bool:
+ """Return the power max entity id"""
+ return self._max_power_sensor_entity_id
+
+ @property
+ def power_sensor_entity_id(self) -> bool:
+ """Return the power entity id"""
+ return self._power_sensor_entity_id
+
+ @property
+ def power_temperature(self) -> bool:
+ """Return the power temperature"""
+ return self._power_temp
+
+ @property
+ def device_power(self) -> bool:
+ """Return the device power"""
+ return self._device_power
+
+ @property
+ def current_power(self) -> bool:
+ """Return the current power from sensor"""
+ return self._current_power
+
+ @property
+ def current_max_power(self) -> bool:
+ """Return the current power from sensor"""
+ return self._current_max_power
+
+ @property
+ def mean_cycle_power(self) -> float | None:
+ """Returns the mean power consumption during the cycle"""
+ if not self._device_power or not self._vtherm.proportional_algorithm:
+ return None
+
+ return float(
+ self._device_power * self._vtherm.proportional_algorithm.on_percent
+ )
+
+ def __str__(self):
+ return f"PowerManager-{self.name}"
diff --git a/custom_components/versatile_thermostat/feature_presence_manager.py b/custom_components/versatile_thermostat/feature_presence_manager.py
new file mode 100644
index 0000000..5b3ab7d
--- /dev/null
+++ b/custom_components/versatile_thermostat/feature_presence_manager.py
@@ -0,0 +1,204 @@
+""" Implements the Presence Feature Manager """
+
+# pylint: disable=line-too-long
+
+import logging
+from typing import Any
+
+from homeassistant.const import (
+ STATE_ON,
+ STATE_OFF,
+ STATE_HOME,
+ STATE_NOT_HOME,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import (
+ HomeAssistant,
+ callback,
+ Event,
+)
+from homeassistant.helpers.event import (
+ async_track_state_change_event,
+ EventStateChangedData,
+)
+
+from homeassistant.components.climate import (
+ PRESET_ACTIVITY,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
+)
+
+from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData
+
+from .base_manager import BaseFeatureManager
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class FeaturePresenceManager(BaseFeatureManager):
+ """The implementation of the Presence feature"""
+
+ unrecorded_attributes = frozenset(
+ {
+ "presence_sensor_entity_id",
+ "is_presence_configured",
+ }
+ )
+
+ def __init__(self, vtherm: Any, hass: HomeAssistant):
+ """Init of a featureManager"""
+ super().__init__(vtherm, hass)
+ self._presence_state: str = STATE_UNAVAILABLE
+ self._presence_sensor_entity_id: str = None
+ self._is_configured: bool = False
+
+ @overrides
+ def post_init(self, entry_infos: ConfigData):
+ """Reinit of the manager"""
+ self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
+ if (
+ entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
+ and self._presence_sensor_entity_id is not None
+ ):
+ self._is_configured = True
+ self._presence_state = STATE_UNKNOWN
+
+ @overrides
+ def start_listening(self):
+ """Start listening the underlying entity"""
+ if self._is_configured:
+ self.stop_listening()
+ self.add_listener(
+ async_track_state_change_event(
+ self.hass,
+ [self._presence_sensor_entity_id],
+ self._presence_sensor_changed,
+ )
+ )
+
+ @overrides
+ async def refresh_state(self) -> bool:
+ """Tries to get the last state from sensor
+ Returns True if a change has been made"""
+ ret = False
+ if self._is_configured:
+ # try to acquire presence entity state
+ presence_state = self.hass.states.get(self._presence_sensor_entity_id)
+ if presence_state and presence_state.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ ret = await self.update_presence(presence_state.state)
+ _LOGGER.debug(
+ "%s - Presence have been retrieved: %s",
+ self,
+ presence_state.state,
+ )
+ return ret
+
+ @callback
+ async def _presence_sensor_changed(self, event: Event[EventStateChangedData]):
+ """Handle presence changes."""
+ new_state = event.data.get("new_state")
+ _LOGGER.info(
+ "%s - Presence changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
+ self,
+ new_state,
+ self._vtherm.preset_mode,
+ PRESET_ACTIVITY,
+ )
+ if new_state is None:
+ return
+
+ if await self.update_presence(new_state.state):
+ await self._vtherm.async_control_heating(force=True)
+
+ async def update_presence(self, new_state: str) -> bool:
+ """Update the value of the presence sensor and update the VTherm state accordingly
+ Return true if a change has been made"""
+
+ _LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
+ old_presence_state = self._presence_state
+ self._presence_state = (
+ STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
+ )
+ if self._vtherm.preset_mode in HIDDEN_PRESETS or self._is_configured is False:
+ _LOGGER.info(
+ "%s - Ignoring presence change cause in Power or Security preset or presence not configured",
+ self,
+ )
+ return old_presence_state != self._presence_state
+
+ if new_state is None or new_state not in (
+ STATE_OFF,
+ STATE_ON,
+ STATE_HOME,
+ STATE_NOT_HOME,
+ ):
+ self._presence_state = STATE_UNKNOWN
+ return old_presence_state != self._presence_state
+
+ if self._vtherm.preset_mode not in [
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
+ PRESET_ACTIVITY,
+ ]:
+ return old_presence_state != self._presence_state
+
+ new_temp = self._vtherm.find_preset_temp(self._vtherm.preset_mode)
+ if new_temp is not None:
+ _LOGGER.debug(
+ "%s - presence change in temperature mode new_temp will be: %.2f",
+ self,
+ new_temp,
+ )
+ await self._vtherm.change_target_temperature(new_temp)
+ self._vtherm.recalculate()
+
+ return True
+
+ return old_presence_state != self._presence_state
+
+ def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
+ """Add some custom attributes"""
+ extra_state_attributes.update(
+ {
+ "presence_sensor_entity_id": self._presence_sensor_entity_id,
+ "presence_state": self._presence_state,
+ "is_presence_configured": self._is_configured,
+ }
+ )
+
+ @overrides
+ @property
+ def is_configured(self) -> bool:
+ """Return True of the presence is configured"""
+ return self._is_configured
+
+ @property
+ def presence_state(self) -> str | None:
+ """Return the current presence state STATE_ON or STATE_OFF
+ or STATE_UNAVAILABLE if not configured"""
+ if not self._is_configured:
+ return STATE_UNAVAILABLE
+ return self._presence_state
+
+ @property
+ def is_absence_detected(self) -> bool:
+ """Return true if the presence is configured and presence sensor is OFF"""
+ return self._is_configured and self._presence_state in [
+ STATE_NOT_HOME,
+ STATE_OFF,
+ ]
+
+ @property
+ def presence_sensor_entity_id(self) -> bool:
+ """Return true if the presence is configured and presence sensor is OFF"""
+ return self._presence_sensor_entity_id
+
+ def __str__(self):
+ return f"PresenceManager-{self.name}"
diff --git a/custom_components/versatile_thermostat/feature_safety_manager.py b/custom_components/versatile_thermostat/feature_safety_manager.py
new file mode 100644
index 0000000..3195efe
--- /dev/null
+++ b/custom_components/versatile_thermostat/feature_safety_manager.py
@@ -0,0 +1,322 @@
+# 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}"
diff --git a/custom_components/versatile_thermostat/feature_window_manager.py b/custom_components/versatile_thermostat/feature_window_manager.py
new file mode 100644
index 0000000..2497e6a
--- /dev/null
+++ b/custom_components/versatile_thermostat/feature_window_manager.py
@@ -0,0 +1,546 @@
+""" Implements the Window Feature Manager """
+
+# pylint: disable=line-too-long
+
+import logging
+from typing import Any
+from datetime import timedelta
+
+from homeassistant.const import (
+ STATE_ON,
+ STATE_OFF,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import (
+ HomeAssistant,
+ callback,
+ Event,
+)
+from homeassistant.helpers.event import (
+ async_track_state_change_event,
+ EventStateChangedData,
+ async_call_later,
+)
+
+from homeassistant.components.climate import HVACMode
+
+from homeassistant.exceptions import ConditionError
+from homeassistant.helpers import condition
+
+from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
+from .commons import ConfigData
+
+from .base_manager import BaseFeatureManager
+from .open_window_algorithm import WindowOpenDetectionAlgorithm
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class FeatureWindowManager(BaseFeatureManager):
+ """The implementation of the Window feature"""
+
+ unrecorded_attributes = frozenset(
+ {
+ "window_sensor_entity_id",
+ "is_window_configured",
+ "is_window_bypass",
+ "window_delay_sec",
+ "window_auto_configured",
+ "window_auto_open_threshold",
+ "window_auto_close_threshold",
+ "window_auto_max_duration",
+ "window_action",
+ }
+ )
+
+ def __init__(self, vtherm: Any, hass: HomeAssistant):
+ """Init of a featureManager"""
+ super().__init__(vtherm, hass)
+ self._window_sensor_entity_id: str = None
+ self._window_state: str = STATE_UNAVAILABLE
+ self._window_auto_open_threshold: float = 0
+ self._window_auto_close_threshold: float = 0
+ self._window_auto_max_duration: int = 0
+ self._window_auto_state: bool = False
+ self._window_auto_algo: WindowOpenDetectionAlgorithm = None
+ self._is_window_bypass: bool = False
+ self._window_action: str = None
+ self._window_delay_sec: int | None = 0
+ self._is_configured: bool = False
+ self._is_window_auto_configured: bool = False
+ self._window_call_cancel: callable = None
+
+ @overrides
+ def post_init(self, entry_infos: ConfigData):
+ """Reinit of the manager"""
+ self.dearm_window_timer()
+
+ self._window_auto_state = STATE_UNAVAILABLE
+ self._window_state = STATE_UNAVAILABLE
+
+ self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
+ self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
+
+ self._window_action = (
+ entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
+ )
+
+ self._window_auto_open_threshold = entry_infos.get(
+ CONF_WINDOW_AUTO_OPEN_THRESHOLD
+ )
+ self._window_auto_close_threshold = entry_infos.get(
+ CONF_WINDOW_AUTO_CLOSE_THRESHOLD
+ )
+ self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
+
+ use_window_feature = entry_infos.get(CONF_USE_WINDOW_FEATURE, False)
+
+ if ( # pylint: disable=too-many-boolean-expressions
+ use_window_feature
+ and self._window_sensor_entity_id is None
+ and self._window_auto_open_threshold is not None
+ and self._window_auto_open_threshold > 0.0
+ and self._window_auto_close_threshold is not None
+ and self._window_auto_max_duration is not None
+ and self._window_auto_max_duration > 0
+ and self._window_action is not None
+ ):
+ self._is_window_auto_configured = True
+ self._window_auto_state = STATE_UNKNOWN
+
+ self._window_auto_algo = WindowOpenDetectionAlgorithm(
+ alert_threshold=self._window_auto_open_threshold,
+ end_alert_threshold=self._window_auto_close_threshold,
+ )
+
+ if self._is_window_auto_configured or (
+ use_window_feature
+ and self._window_sensor_entity_id is not None
+ and self._window_delay_sec is not None
+ and self._window_action is not None
+ ):
+ self._is_configured = True
+ self._window_state = STATE_UNKNOWN
+
+ @overrides
+ def start_listening(self):
+ """Start listening the underlying entity"""
+ if self._is_configured:
+ self.stop_listening()
+ if self._window_sensor_entity_id:
+ self.add_listener(
+ async_track_state_change_event(
+ self.hass,
+ [self._window_sensor_entity_id],
+ self._window_sensor_changed,
+ )
+ )
+
+ @overrides
+ def stop_listening(self):
+ """Stop listening and remove the eventual timer still running"""
+ self.dearm_window_timer()
+ super().stop_listening()
+
+ def dearm_window_timer(self):
+ """Dearm the eventual motion time running"""
+ if self._window_call_cancel:
+ self._window_call_cancel()
+ self._window_call_cancel = None
+
+ @overrides
+ async def refresh_state(self) -> bool:
+ """Tries to get the last state from sensor
+ Returns True if a change has been made"""
+ ret = False
+ if self._is_configured and self._window_sensor_entity_id is not None:
+
+ window_state = self.hass.states.get(self._window_sensor_entity_id)
+ if window_state and window_state.state not in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ ):
+ _LOGGER.debug(
+ "%s - Window state have been retrieved: %s",
+ self,
+ self._window_state,
+ )
+ # recalculate the right target_temp in activity mode
+ ret = await self.update_window_state(window_state.state)
+
+ return ret
+
+ @callback
+ async def _window_sensor_changed(self, event: Event[EventStateChangedData]):
+ """Handle window sensor changes."""
+ new_state = event.data.get("new_state")
+ old_state = event.data.get("old_state")
+ _LOGGER.info(
+ "%s - Window changed. Event.new_state is %s, _hvac_mode=%s, _saved_hvac_mode=%s",
+ self,
+ new_state,
+ self._vtherm.hvac_mode,
+ self._vtherm.saved_hvac_mode,
+ )
+
+ # Check delay condition
+ async def try_window_condition(_):
+ try:
+ long_enough = condition.state(
+ self._hass,
+ self._window_sensor_entity_id,
+ new_state.state,
+ timedelta(seconds=self._window_delay_sec),
+ )
+ except ConditionError:
+ long_enough = False
+
+ if not long_enough:
+ _LOGGER.debug(
+ "Window delay condition is not satisfied. Ignore window event"
+ )
+ self._window_state = old_state.state or STATE_OFF
+ return
+
+ _LOGGER.debug("%s - Window delay condition is satisfied", self)
+
+ if self._window_state == new_state.state:
+ _LOGGER.debug("%s - no change in window state. Forget the event")
+ return
+
+ _LOGGER.debug("%s - Window ByPass is : %s", self, self._is_window_bypass)
+ if self._is_window_bypass:
+ _LOGGER.info(
+ "%s - Window ByPass is activated. Ignore window event", self
+ )
+ # We change tne state but we don't apply the change
+ self._window_state = new_state.state
+ else:
+ await self.update_window_state(new_state.state)
+
+ self._vtherm.update_custom_attributes()
+
+ if new_state is None or old_state is None or new_state.state == old_state.state:
+ return try_window_condition
+
+ self.dearm_window_timer()
+ self._window_call_cancel = async_call_later(
+ self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition
+ )
+ # For testing purpose we need to access the inner function
+ return try_window_condition
+
+ async def update_window_state(self, new_state: str = None) -> bool:
+ """Change the window detection state.
+ new_state is on if an open window have been detected or off else
+ return True if the state have changed
+ """
+
+ if self._window_state == new_state:
+ return False
+
+ if new_state != STATE_ON:
+ _LOGGER.info(
+ "%s - Window is closed. Restoring hvac_mode '%s' if stopped by window detection or temperature %s",
+ self,
+ self._vtherm.saved_hvac_mode,
+ self._vtherm.saved_target_temp,
+ )
+
+ if self._window_action in [
+ CONF_WINDOW_FROST_TEMP,
+ CONF_WINDOW_ECO_TEMP,
+ ]:
+ await self._vtherm.restore_target_temp()
+
+ # default to TURN_OFF
+ elif self._window_action in [CONF_WINDOW_TURN_OFF]:
+ if (
+ self._vtherm.last_central_mode != CENTRAL_MODE_STOPPED
+ and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
+ ):
+ self._vtherm.set_hvac_off_reason(None)
+ await self._vtherm.restore_hvac_mode(True)
+ elif self._window_action in [CONF_WINDOW_FAN_ONLY]:
+ if self._vtherm.last_central_mode != CENTRAL_MODE_STOPPED:
+ self._vtherm.set_hvac_off_reason(None)
+ await self._vtherm.restore_hvac_mode(True)
+ else:
+ _LOGGER.error(
+ "%s - undefined window_action %s. Please open a bug in the github of this project with this log",
+ self,
+ self._window_action,
+ )
+ return False
+ else:
+ _LOGGER.info(
+ "%s - Window is open. Apply window action %s", self, self._window_action
+ )
+ if self._window_action == CONF_WINDOW_TURN_OFF and not self._vtherm.is_on:
+ _LOGGER.debug(
+ "%s is already off. Forget turning off VTherm due to window detection"
+ )
+ self._window_state = new_state
+ return False
+
+ # self._window_state = new_state
+ if self._vtherm.last_central_mode in [CENTRAL_MODE_AUTO, None]:
+ if self._window_action in [
+ CONF_WINDOW_TURN_OFF,
+ CONF_WINDOW_FAN_ONLY,
+ ]:
+ self._vtherm.save_hvac_mode()
+ elif self._window_action in [
+ CONF_WINDOW_FROST_TEMP,
+ CONF_WINDOW_ECO_TEMP,
+ ]:
+ self._vtherm.save_target_temp()
+
+ if (
+ self._window_action == CONF_WINDOW_FAN_ONLY
+ and HVACMode.FAN_ONLY in self._vtherm.hvac_modes
+ ):
+ await self._vtherm.async_set_hvac_mode(HVACMode.FAN_ONLY)
+ elif (
+ self._window_action == CONF_WINDOW_FROST_TEMP
+ and self._vtherm.is_preset_configured(PRESET_FROST_PROTECTION)
+ ):
+ await self._vtherm.change_target_temperature(
+ self._vtherm.find_preset_temp(PRESET_FROST_PROTECTION)
+ )
+ elif (
+ self._window_action == CONF_WINDOW_ECO_TEMP
+ and self._vtherm.is_preset_configured(PRESET_ECO)
+ ):
+ await self._vtherm.change_target_temperature(
+ self._vtherm.find_preset_temp(PRESET_ECO)
+ )
+ else: # default is to turn_off
+ self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
+ await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
+
+ self._window_state = new_state
+ return True
+
+ async def manage_window_auto(self, in_cycle=False) -> callable:
+ """The management of the window auto feature
+ Returns the dearm function used to deactivate the window auto"""
+
+ async def dearm_window_auto(_):
+ """Callback that will be called after end of WINDOW_AUTO_MAX_DURATION"""
+ _LOGGER.info("Unset window auto because MAX_DURATION is exceeded")
+ await deactivate_window_auto(auto=True)
+
+ async def deactivate_window_auto(auto=False):
+ """Deactivation of the Window auto state"""
+ _LOGGER.warning(
+ "%s - End auto detection of open window slope=%.3f", self, slope
+ )
+ # Send an event
+ cause = "max duration expiration" if auto else "end of slope alert"
+ self._vtherm.send_event(
+ EventType.WINDOW_AUTO_EVENT,
+ {"type": "end", "cause": cause, "curve_slope": slope},
+ )
+ # Set attributes
+ self._window_auto_state = STATE_OFF
+ await self.update_window_state(self._window_auto_state)
+ # await self.restore_hvac_mode(True)
+
+ self.dearm_window_timer()
+
+ if not self._window_auto_algo:
+ return None
+
+ if in_cycle:
+ slope = self._window_auto_algo.check_age_last_measurement(
+ temperature=self._vtherm.ema_temperature,
+ datetime_now=self._vtherm.now,
+ )
+ else:
+ slope = self._window_auto_algo.add_temp_measurement(
+ temperature=self._vtherm.ema_temperature,
+ datetime_measure=self._vtherm.last_temperature_measure,
+ )
+
+ _LOGGER.debug(
+ "%s - Window auto is on, check the alert. last slope is %.3f",
+ self,
+ slope if slope is not None else 0.0,
+ )
+
+ if self.is_window_bypass or not self._is_window_auto_configured:
+ _LOGGER.debug(
+ "%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
+ self,
+ )
+ return None
+
+ if (
+ self._window_auto_algo.is_window_open_detected()
+ and self._window_auto_state in [STATE_UNKNOWN, STATE_OFF]
+ and self._vtherm.hvac_mode != HVACMode.OFF
+ ):
+ if (
+ self._vtherm.proportional_algorithm
+ and self._vtherm.proportional_algorithm.on_percent <= 0.0
+ ):
+ _LOGGER.info(
+ "%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
+ self,
+ slope,
+ )
+ return dearm_window_auto
+
+ _LOGGER.warning(
+ "%s - Start auto detection of open window slope=%.3f", self, slope
+ )
+
+ # Send an event
+ self._vtherm.send_event(
+ EventType.WINDOW_AUTO_EVENT,
+ {"type": "start", "cause": "slope alert", "curve_slope": slope},
+ )
+ # Set attributes
+ self._window_auto_state = STATE_ON
+ await self.update_window_state(self._window_auto_state)
+
+ # Arm the end trigger
+ self.dearm_window_timer()
+ self._window_call_cancel = async_call_later(
+ self.hass,
+ timedelta(minutes=self._window_auto_max_duration),
+ dearm_window_auto,
+ )
+
+ elif (
+ self._window_auto_algo.is_window_close_detected()
+ and self._window_auto_state == STATE_ON
+ ):
+ await deactivate_window_auto(False)
+
+ # For testing purpose we need to return the inner function
+ return dearm_window_auto
+
+ def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
+ """Add some custom attributes"""
+ extra_state_attributes.update(
+ {
+ "window_state": self.window_state,
+ "window_auto_state": self.window_auto_state,
+ "window_action": self.window_action,
+ "is_window_bypass": self._is_window_bypass,
+ "window_sensor_entity_id": self._window_sensor_entity_id,
+ "window_delay_sec": self._window_delay_sec,
+ "is_window_configured": self._is_configured,
+ "is_window_auto_configured": self._is_window_auto_configured,
+ "window_auto_open_threshold": self._window_auto_open_threshold,
+ "window_auto_close_threshold": self._window_auto_close_threshold,
+ "window_auto_max_duration": self._window_auto_max_duration,
+ }
+ )
+
+ async def set_window_bypass(self, window_bypass: bool) -> bool:
+ """Set the window bypass flag
+ Return True if state have been changed"""
+ self._is_window_bypass = window_bypass
+ if not self._is_window_bypass and self._window_state:
+ _LOGGER.info(
+ "%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
+ self,
+ HVACMode.OFF,
+ )
+ self._vtherm.save_hvac_mode()
+ await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
+ return True
+
+ if self._is_window_bypass and self._window_state:
+ _LOGGER.info(
+ "%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
+ self,
+ )
+ await self._vtherm.restore_hvac_mode(True)
+ return True
+ return False
+
+ @overrides
+ @property
+ def is_configured(self) -> bool:
+ """Return True of the window feature is configured"""
+ return self._is_configured
+
+ @property
+ def is_window_auto_configured(self) -> bool:
+ """Return True of the window automatic detection is configured"""
+ return self._is_window_auto_configured
+
+ @property
+ def window_state(self) -> str | None:
+ """Return the current window state STATE_ON or STATE_OFF
+ or STATE_UNAVAILABLE if not configured"""
+ if not self._is_configured:
+ return STATE_UNAVAILABLE
+ return self._window_state
+
+ @property
+ def window_auto_state(self) -> str | None:
+ """Return the current window auto state STATE_ON or STATE_OFF
+ or STATE_UNAVAILABLE if not configured"""
+ if not self._is_configured:
+ return STATE_UNAVAILABLE
+ return self._window_auto_state
+
+ @property
+ def is_window_bypass(self) -> str | None:
+ """Return True if the window bypass is activated"""
+ if not self._is_configured:
+ return False
+ return self._is_window_bypass
+
+ @property
+ def is_window_detected(self) -> bool:
+ """Return true if the presence is configured and presence sensor is OFF"""
+ return self._is_configured and (
+ self._window_state == STATE_ON or self._window_auto_state == STATE_ON
+ )
+
+ @property
+ def window_sensor_entity_id(self) -> bool:
+ """Return true if the presence is configured and presence sensor is OFF"""
+ return self._window_sensor_entity_id
+
+ @property
+ def window_delay_sec(self) -> bool:
+ """Return the motion delay"""
+ return self._window_delay_sec
+
+ @property
+ def window_action(self) -> bool:
+ """Return the window action"""
+ return self._window_action
+
+ @property
+ def window_auto_open_threshold(self) -> bool:
+ """Return the window_auto_open_threshold"""
+ return self._window_auto_open_threshold
+
+ @property
+ def window_auto_close_threshold(self) -> bool:
+ """Return the window_auto_close_threshold"""
+ return self._window_auto_close_threshold
+
+ @property
+ def window_auto_max_duration(self) -> bool:
+ """Return the window_auto_max_duration"""
+ return self._window_auto_max_duration
+
+ @property
+ def last_slope(self) -> float:
+ """Return the last slope (in °C/hour)"""
+ if not self._window_auto_algo:
+ return None
+ return self._window_auto_algo.last_slope
+
+ def __str__(self):
+ return f"WindowManager-{self.name}"
diff --git a/custom_components/versatile_thermostat/manifest.json b/custom_components/versatile_thermostat/manifest.json
index 1fc9cca..9651480 100644
--- a/custom_components/versatile_thermostat/manifest.json
+++ b/custom_components/versatile_thermostat/manifest.json
@@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
- "version": "6.8.4",
+ "version": "7.0.0",
"zeroconf": []
}
\ No newline at end of file
diff --git a/custom_components/versatile_thermostat/number.py b/custom_components/versatile_thermostat/number.py
index af9b169..d1be29d 100644
--- a/custom_components/versatile_thermostat/number.py
+++ b/custom_components/versatile_thermostat/number.py
@@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from .vtherm_api import VersatileThermostatAPI
-from .commons import VersatileThermostatBaseEntity
+from .base_entity import VersatileThermostatBaseEntity
from .const import (
DOMAIN,
@@ -367,9 +367,6 @@ class CentralConfigTemperatureNumber(
@property
def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement"""
- # TODO Kelvin ? It seems not because all internal values are stored in
- # ° Celsius but only the render in front can be in °K depending on the
- # user configuration.
return self.hass.config.units.temperature_unit
diff --git a/custom_components/versatile_thermostat/prop_algorithm.py b/custom_components/versatile_thermostat/prop_algorithm.py
index 2feb26b..f37747f 100644
--- a/custom_components/versatile_thermostat/prop_algorithm.py
+++ b/custom_components/versatile_thermostat/prop_algorithm.py
@@ -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
diff --git a/custom_components/versatile_thermostat/sensor.py b/custom_components/versatile_thermostat/sensor.py
index 23d624b..502d6e8 100644
--- a/custom_components/versatile_thermostat/sensor.py
+++ b/custom_components/versatile_thermostat/sensor.py
@@ -35,7 +35,7 @@ from homeassistant.components.climate import (
from .base_thermostat import BaseThermostat
from .vtherm_api import VersatileThermostatAPI
-from .commons import VersatileThermostatBaseEntity
+from .base_entity import VersatileThermostatBaseEntity
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
@@ -165,7 +165,7 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
if not self.my_climate:
return None
- if self.my_climate.device_power > THRESHOLD_WATT_KILO:
+ if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
return UnitOfEnergy.WATT_HOUR
else:
return UnitOfEnergy.KILO_WATT_HOUR
@@ -190,16 +190,17 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Called when my climate have change"""
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
- if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
- self.my_climate.mean_cycle_power
- ):
+ if math.isnan(
+ float(self.my_climate.power_manager.mean_cycle_power)
+ ) or math.isinf(self.my_climate.power_manager.mean_cycle_power):
raise ValueError(
- f"Sensor has illegal state {self.my_climate.mean_cycle_power}"
+ f"Sensor has illegal state {self.my_climate.power_manager.mean_cycle_power}"
)
old_state = self._attr_native_value
self._attr_native_value = round(
- self.my_climate.mean_cycle_power, self.suggested_display_precision
+ self.my_climate.power_manager.mean_cycle_power,
+ self.suggested_display_precision,
)
if old_state != self._attr_native_value:
self.async_write_ha_state()
@@ -222,7 +223,7 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
if not self.my_climate:
return None
- if self.my_climate.device_power > THRESHOLD_WATT_KILO:
+ if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
return UnitOfPower.WATT
else:
return UnitOfPower.KILO_WATT
diff --git a/custom_components/versatile_thermostat/services.yaml b/custom_components/versatile_thermostat/services.yaml
index 2168fa0..82da3ad 100644
--- a/custom_components/versatile_thermostat/services.yaml
+++ b/custom_components/versatile_thermostat/services.yaml
@@ -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:
diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json
index 5a43d68..491ce22 100644
--- a/custom_components/versatile_thermostat/strings.json
+++ b/custom_components/versatile_thermostat/strings.json
@@ -192,16 +192,16 @@
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimum activation delay",
- "security_delay_min": "Safety delay (in minutes)",
- "security_min_on_percent": "Minimum power percent to enable safety mode",
- "security_default_on_percent": "Power percent to use in safety mode",
+ "safety_delay_min": "Safety delay (in minutes)",
+ "safety_min_on_percent": "Minimum power percent to enable safety mode",
+ "safety_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
- "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
- "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
- "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
+ "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
+ "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
+ "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
@@ -438,16 +438,16 @@
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimum activation delay",
- "security_delay_min": "Safety delay (in minutes)",
- "security_min_on_percent": "Minimum power percent to enable safety mode",
- "security_default_on_percent": "Power percent to use in safety mode",
+ "safety_delay_min": "Safety delay (in minutes)",
+ "safety_min_on_percent": "Minimum power percent to enable safety mode",
+ "safety_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
- "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
- "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
- "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
+ "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
+ "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
+ "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
diff --git a/custom_components/versatile_thermostat/switch.py b/custom_components/versatile_thermostat/switch.py
index 754eaef..9801f98 100644
--- a/custom_components/versatile_thermostat/switch.py
+++ b/custom_components/versatile_thermostat/switch.py
@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .commons import VersatileThermostatBaseEntity
+from .base_entity import VersatileThermostatBaseEntity
from .const import * # pylint: disable=unused-wildcard-import,wildcard-import
@@ -84,8 +84,13 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
def update_my_state_and_vtherm(self):
"""Update the auto_start_stop_enable flag in my VTherm"""
self.async_write_ha_state()
- if self.my_climate is not None:
- self.my_climate.set_auto_start_stop_enable(self._attr_is_on)
+ if (
+ self.my_climate is not None
+ and self.my_climate.auto_start_stop_manager is not None
+ ):
+ self.my_climate.auto_start_stop_manager.set_auto_start_stop_enable(
+ self._attr_is_on
+ )
@callback
async def async_turn_on(self, **kwargs: Any) -> None:
diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py
index 92938e9..5473f44 100644
--- a/custom_components/versatile_thermostat/thermostat_climate.py
+++ b/custom_components/versatile_thermostat/thermostat_climate.py
@@ -24,11 +24,7 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingClimate
-from .auto_start_stop_algorithm import (
- AutoStartStopDetectionAlgorithm,
- AUTO_START_STOP_ACTION_OFF,
- AUTO_START_STOP_ACTION_ON,
-)
+from .feature_auto_start_stop_manager import FeatureAutoStartStopManager
_LOGGER = logging.getLogger(__name__)
@@ -55,15 +51,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_activated_fan_mode",
"auto_deactivated_fan_mode",
"auto_regulation_use_device_temp",
- "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",
"follow_underlying_temp_change",
}
- )
+ ).union(FeatureAutoStartStopManager.unrecorded_attributes)
)
def __init__(
@@ -83,11 +73,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# The fan_mode name depending of the current_mode
self._auto_activated_fan_mode: str | None = None
self._auto_deactivated_fan_mode: str | None = None
- self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
- AUTO_START_STOP_LEVEL_NONE
- )
- self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
- self._is_auto_start_stop_enabled: bool = False
self._follow_underlying_temp_change: bool = False
self._last_regulation_change = None # NowClass.get_now(hass)
@@ -99,6 +84,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""
+ self._auto_start_stop_manager: FeatureAutoStartStopManager = (
+ FeatureAutoStartStopManager(self, self._hass)
+ )
+
+ self.register_manager(self._auto_start_stop_manager)
+
super().post_init(config_entry)
for climate in config_entry.get(CONF_UNDERLYING_LIST):
@@ -136,19 +127,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
)
- use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
- if use_auto_start_stop:
- self._auto_start_stop_level = config_entry.get(
- CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
- )
- else:
- 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
- )
-
@property
def is_over_climate(self) -> bool:
"""True if the Thermostat is over_climate"""
@@ -178,9 +156,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return self.calculate_hvac_action(self._underlyings)
@overrides
- async def _async_internal_set_temperature(self, temperature: float):
+ async def change_target_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any"""
- await super()._async_internal_set_temperature(temperature)
+ await super().change_target_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature)
# Is necessary cause control_heating method will not force the update.
@@ -538,28 +516,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
self.auto_regulation_use_device_temp
)
- self._attr_extra_state_attributes["auto_start_stop_enable"] = (
- self.auto_start_stop_enable
- )
-
- self._attr_extra_state_attributes["auto_start_stop_level"] = (
- self._auto_start_stop_algo.level
- )
- self._attr_extra_state_attributes["auto_start_stop_dtmin"] = (
- self._auto_start_stop_algo.dt_min
- )
- self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = (
- self._auto_start_stop_algo.accumulated_error
- )
-
- self._attr_extra_state_attributes[
- "auto_start_stop_accumulated_error_threshold"
- ] = self._auto_start_stop_algo.accumulated_error_threshold
-
- self._attr_extra_state_attributes["auto_start_stop_last_switch_date"] = (
- self._auto_start_stop_algo.last_switch_date
- )
-
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
self._follow_underlying_temp_change
)
@@ -593,7 +549,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
_LOGGER.info(
"%s - Force resent target temp cause we turn on some over climate"
)
- await self._async_internal_set_temperature(self._target_temp)
+ await self.change_target_temperature(self._target_temp)
@overrides
def incremente_energy(self):
@@ -602,13 +558,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
if self.hvac_mode == HVACMode.OFF:
return
+ device_power = self.power_manager.device_power
added_energy = 0
if (
self.is_over_climate
and self._underlying_climate_delta_t is not None
- and self._device_power
+ and device_power
):
- added_energy = self._device_power * self._underlying_climate_delta_t
+ added_energy = device_power * self._underlying_climate_delta_t
if self._total_energy is None:
self._total_energy = added_energy
@@ -898,90 +855,17 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await end_climate_changed(changes)
- async def check_auto_start_stop(self):
- """Check the auto-start-stop and an eventual action
- Return False if we should stop the control_heating method"""
- slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
- action = self._auto_start_stop_algo.calculate_action(
- self.hvac_mode,
- self._saved_hvac_mode,
- self.target_temperature,
- self.current_temperature,
- slope,
- self.now,
- )
- _LOGGER.debug("%s - auto_start_stop action is %s", self, action)
- if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
- _LOGGER.info(
- "%s - Turning OFF the Vtherm due to auto-start-stop conditions",
- self,
- )
- self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
- await self.async_turn_off()
-
- # Send an event
- self.send_event(
- event_type=EventType.AUTO_START_STOP_EVENT,
- data={
- "type": "stop",
- "name": self.name,
- "cause": "Auto stop conditions reached",
- "hvac_mode": self.hvac_mode,
- "saved_hvac_mode": self._saved_hvac_mode,
- "target_temperature": self.target_temperature,
- "current_temperature": self.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,
- },
- )
-
- # Stop here
- return False
- elif (
- action == AUTO_START_STOP_ACTION_ON
- and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
- ):
- _LOGGER.info(
- "%s - Turning ON the Vtherm due to auto-start-stop conditions", self
- )
- await self.async_turn_on()
-
- # Send an event
- self.send_event(
- event_type=EventType.AUTO_START_STOP_EVENT,
- data={
- "type": "start",
- "name": self.name,
- "cause": "Auto start conditions reached",
- "hvac_mode": self.hvac_mode,
- "saved_hvac_mode": self._saved_hvac_mode,
- "target_temperature": self.target_temperature,
- "current_temperature": self.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.update_custom_attributes()
-
- return True
-
@overrides
async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _)
# Check if we need to auto start/stop the Vtherm
- if self.auto_start_stop_enable:
- continu = await self.check_auto_start_stop()
- if not continu:
- return ret
- else:
- _LOGGER.debug("%s - auto start/stop is disabled", self)
+ continu = await self.auto_start_stop_manager.refresh_state()
+ if not continu:
+ return ret
- # Continue the normal async_control_heating
+ # Continue the normal async_control_heating
# Send the regulated temperature to the underlyings
await self._send_regulated_temperature()
@@ -991,37 +875,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return ret
- def set_auto_start_stop_enable(self, is_enabled: bool):
- """Enable/Disable the auto-start/stop feature"""
- self._is_auto_start_stop_enabled = is_enabled
- if (
- self.hvac_mode == HVACMode.OFF
- and self.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"
- )
- self.hass.create_task(self.async_turn_on())
-
- # Send an event
- self.send_event(
- event_type=EventType.AUTO_START_STOP_EVENT,
- data={
- "type": "start",
- "name": self.name,
- "cause": "Auto start stop disabled",
- "hvac_mode": self.hvac_mode,
- "saved_hvac_mode": self._saved_hvac_mode,
- "target_temperature": self.target_temperature,
- "current_temperature": self.current_temperature,
- "temperature_slope": round(self.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,
- },
- )
-
- self.update_custom_attributes()
-
def set_follow_underlying_temp_change(self, follow: bool):
"""Set the flaf follow the underlying temperature changes"""
self._follow_underlying_temp_change = follow
@@ -1172,21 +1025,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return False
return True
- @property
- def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
- """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 follow_underlying_temp_change(self) -> bool:
"""Get the follow underlying temp change flag"""
return self._follow_underlying_temp_change
+ @property
+ def auto_start_stop_manager(self) -> FeatureAutoStartStopManager:
+ """Return the auto-start-stop Manager"""
+ return self._auto_start_stop_manager
+
@overrides
def init_underlyings(self):
"""Init the underlyings if not already done"""
diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py
index 3fcb473..6fbfda6 100644
--- a/custom_components/versatile_thermostat/thermostat_switch.py
+++ b/custom_components/versatile_thermostat/thermostat_switch.py
@@ -182,8 +182,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
return
added_energy = 0
- if not self.is_over_climate and self.mean_cycle_power is not None:
- added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
+ if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
+ added_energy = (
+ self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
+ )
if self._total_energy is None:
self._total_energy = added_energy
diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py
index ec05254..207a2da 100644
--- a/custom_components/versatile_thermostat/thermostat_valve.py
+++ b/custom_components/versatile_thermostat/thermostat_valve.py
@@ -265,8 +265,10 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
return
added_energy = 0
- if not self.is_over_climate and self.mean_cycle_power is not None:
- added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
+ if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
+ added_energy = (
+ self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
+ )
if self._total_energy is None:
self._total_energy = added_energy
diff --git a/custom_components/versatile_thermostat/translations/el.json b/custom_components/versatile_thermostat/translations/el.json
index 5ae6efb..3c3df7a 100644
--- a/custom_components/versatile_thermostat/translations/el.json
+++ b/custom_components/versatile_thermostat/translations/el.json
@@ -152,15 +152,15 @@
"description": "Διαμόρφωση των προχωρημένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές αν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
"data": {
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
- "security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
- "security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
- "security_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
+ "safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
+ "safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
+ "safety_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
},
"data_description": {
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί",
- "security_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
- "security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
- "security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
+ "safety_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
+ "safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
+ "safety_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
}
}
},
@@ -325,15 +325,15 @@
"description": "Διαμόρφωση των προηγμένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές εάν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
"data": {
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
- "security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
- "security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
- "security_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
+ "safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
+ "safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
+ "safety_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
},
"data_description": {
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί",
- "security_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
- "security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
- "security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
+ "safety_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
+ "safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
+ "safety_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
}
}
},
diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json
index 1a48eaf..160c686 100644
--- a/custom_components/versatile_thermostat/translations/en.json
+++ b/custom_components/versatile_thermostat/translations/en.json
@@ -192,16 +192,16 @@
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimum activation delay",
- "security_delay_min": "Safety delay (in minutes)",
- "security_min_on_percent": "Minimum power percent to enable safety mode",
- "security_default_on_percent": "Power percent to use in safety mode",
+ "safety_delay_min": "Safety delay (in minutes)",
+ "safety_min_on_percent": "Minimum power percent to enable safety mode",
+ "safety_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
- "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
- "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
- "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
+ "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
+ "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
+ "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
@@ -438,16 +438,16 @@
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimum activation delay",
- "security_delay_min": "Safety delay (in minutes)",
- "security_min_on_percent": "Minimum power percent to enable safety mode",
- "security_default_on_percent": "Power percent to use in safety mode",
+ "safety_delay_min": "Safety delay (in minutes)",
+ "safety_min_on_percent": "Minimum power percent to enable safety mode",
+ "safety_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
- "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
- "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
- "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
+ "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
+ "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
+ "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json
index 3b2fb04..c3c2cfe 100644
--- a/custom_components/versatile_thermostat/translations/fr.json
+++ b/custom_components/versatile_thermostat/translations/fr.json
@@ -192,16 +192,16 @@
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai minimal d'activation",
- "security_delay_min": "Délai maximal entre 2 mesures de températures",
- "security_min_on_percent": "Pourcentage minimal de puissance",
- "security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
+ "safety_delay_min": "Délai maximal entre 2 mesures de températures",
+ "safety_min_on_percent": "Pourcentage minimal de puissance",
+ "safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
- "security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
- "security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
- "security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
+ "safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
+ "safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
+ "safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
}
},
@@ -432,16 +432,16 @@
"description": "Paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai minimal d'activation",
- "security_delay_min": "Délai maximal entre 2 mesures de températures",
- "security_min_on_percent": "Pourcentage minimal de puissance",
- "security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
+ "safety_delay_min": "Délai maximal entre 2 mesures de températures",
+ "safety_min_on_percent": "Pourcentage minimal de puissance",
+ "safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
- "security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
- "security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
- "security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
+ "safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
+ "safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
+ "safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
}
},
diff --git a/custom_components/versatile_thermostat/translations/it.json b/custom_components/versatile_thermostat/translations/it.json
index 7ced3b3..2de718a 100644
--- a/custom_components/versatile_thermostat/translations/it.json
+++ b/custom_components/versatile_thermostat/translations/it.json
@@ -143,15 +143,15 @@
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
- "security_delay_min": "Ritardo di sicurezza (in minuti)",
- "security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
- "security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
+ "safety_delay_min": "Ritardo di sicurezza (in minuti)",
+ "safety_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
+ "safety_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
- "security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
- "security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
- "security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
+ "safety_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
+ "safety_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
+ "safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
@@ -307,15 +307,15 @@
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
- "security_delay_min": "Ritardo di sicurezza (in minuti)",
- "security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
- "security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
+ "safety_delay_min": "Ritardo di sicurezza (in minuti)",
+ "safety_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
+ "safety_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
- "security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
- "security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
- "security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
+ "safety_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
+ "safety_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
+ "safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
diff --git a/custom_components/versatile_thermostat/translations/sk.json b/custom_components/versatile_thermostat/translations/sk.json
index 6aa50b0..5cceaef 100644
--- a/custom_components/versatile_thermostat/translations/sk.json
+++ b/custom_components/versatile_thermostat/translations/sk.json
@@ -211,16 +211,16 @@
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
"data": {
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
- "security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
- "security_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
- "security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
+ "safety_delay_min": "Bezpečnostné oneskorenie (v minútach)",
+ "safety_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
+ "safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
},
"data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
- "security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
- "security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
- "security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
+ "safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
+ "safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
+ "safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
}
}
@@ -446,16 +446,16 @@
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
"data": {
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
- "security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
- "security_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
- "security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
+ "safety_delay_min": "Bezpečnostné oneskorenie (v minútach)",
+ "safety_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
+ "safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
},
"data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
- "security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
- "security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
- "security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
+ "safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
+ "safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
+ "safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
}
}
@@ -578,4 +578,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py
index 90cac59..a79a918 100644
--- a/custom_components/versatile_thermostat/underlyings.py
+++ b/custom_components/versatile_thermostat/underlyings.py
@@ -409,11 +409,11 @@ class UnderlyingSwitch(UnderlyingEntity):
await self.turn_off()
return
- if await self._thermostat.check_overpowering():
+ if await self._thermostat.power_manager.check_overpowering():
_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"
diff --git a/custom_components/versatile_thermostat/vtherm_api.py b/custom_components/versatile_thermostat/vtherm_api.py
index 3ae8e8f..16f370d 100644
--- a/custom_components/versatile_thermostat/vtherm_api.py
+++ b/custom_components/versatile_thermostat/vtherm_api.py
@@ -125,15 +125,10 @@ class VersatileThermostatAPI(dict):
):
"""register the two number entities needed for boiler activation"""
self._threshold_number_entity = threshold_number_entity
- # If sensor and threshold number are initialized, reload the listener
- # if self._nb_active_number_entity and self._central_boiler_entity:
- # self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_nb_device_active_boiler(self, nb_active_number_entity):
"""register the two number entities needed for boiler activation"""
self._nb_active_number_entity = nb_active_number_entity
- # if self._threshold_number_entity and self._central_boiler_entity:
- # self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_temperature_number(
self,
@@ -172,13 +167,6 @@ class VersatileThermostatAPI(dict):
)
if component:
for entity in component.entities:
- # if hasattr(entity, "init_presets"):
- # if (
- # only_use_central is False
- # or entity.use_central_config_temperature
- # ):
- # await entity.init_presets(self.find_central_configuration())
-
# A little hack to test if the climate is a VTherm. Cannot use isinstance
# due to circular dependency of BaseThermostat
if (
diff --git a/documentation/en/feature-advanced.md b/documentation/en/feature-advanced.md
index 158a18b..9bb0371 100644
--- a/documentation/en/feature-advanced.md
+++ b/documentation/en/feature-advanced.md
@@ -27,12 +27,12 @@ The first delay (`minimal_activation_delay_sec`) in seconds is the minimum accep
### Safety Mode
-The second delay (`security_delay_min`) is the maximum time between two temperature measurements before the _VTherm_ switches to Safety Mode.
+The second delay (`safety_delay_min`) is the maximum time between two temperature measurements before the _VTherm_ switches to Safety Mode.
-The third parameter (`security_min_on_percent`) is the minimum `on_percent` below which Safety Mode will not be activated. This setting prevents activating Safety Mode if the controlled radiator does not heat sufficiently. In this case, there is no physical risk to the home, only the risk of overheating or underheating.
+The third parameter (`safety_min_on_percent`) is the minimum `on_percent` below which Safety Mode will not be activated. This setting prevents activating Safety Mode if the controlled radiator does not heat sufficiently. In this case, there is no physical risk to the home, only the risk of overheating or underheating.
Setting this parameter to `0.00` will trigger Safety Mode regardless of the last heating setting, whereas `1.00` will never trigger Safety Mode (effectively disabling the feature). This can be useful to adapt the safety mechanism to your specific needs.
-The fourth parameter (`security_default_on_percent`) defines the `on_percent` used when the thermostat switches to `security` mode. Setting it to `0` will turn off the thermostat in Safety Mode, while setting it to a value like `0.2` (20%) ensures some heating remains, avoiding a completely frozen home in case of a thermometer failure.
+The fourth parameter (`safety_default_on_percent`) defines the `on_percent` used when the thermostat switches to `security` mode. Setting it to `0` will turn off the thermostat in Safety Mode, while setting it to a value like `0.2` (20%) ensures some heating remains, avoiding a completely frozen home in case of a thermometer failure.
It is possible to disable Safety Mode triggered by missing data from the outdoor thermometer. Since the outdoor thermometer usually has a minor impact on regulation (depending on your configuration), it might not be critical if it's unavailable. To do this, add the following lines to your `configuration.yaml`:
@@ -49,5 +49,5 @@ By default, the outdoor thermometer can trigger Safety Mode if it stops sending
> 1. When the temperature sensor resumes reporting, the preset will be restored to its previous value.
> 2. Two temperature sources are required: the indoor and outdoor temperatures. Both must report values, or the thermostat will switch to "security" preset.
> 3. An action is available to adjust the three safety parameters. This can help adapt Safety Mode to your needs.
-> 4. For normal use, `security_default_on_percent` should be lower than `security_min_on_percent`.
+> 4. For normal use, `safety_default_on_percent` should be lower than `safety_min_on_percent`.
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), a _VTherm_ in Safety Mode is indicated by a gray overlay showing the faulty thermometer and the time since its last value update: .
\ No newline at end of file
diff --git a/documentation/en/one-page.md b/documentation/en/one-page.md
index 9fe2af5..ea7ae99 100644
--- a/documentation/en/one-page.md
+++ b/documentation/en/one-page.md
@@ -678,14 +678,14 @@ Le formulaire de configuration avancée est le suivant :
Le premier délai (minimal_activation_delay_sec) en secondes est le délai minimum acceptable pour allumer le chauffage. Lorsque le calcul donne un délai de mise sous tension inférieur à cette valeur, le chauffage reste éteint.
-Le deuxième délai (``security_delay_min``) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security``. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur passeront en mode ``security`` après ce délai. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible.
+Le deuxième délai (``safety_delay_min``) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security``. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur passeront en mode ``security`` après ce délai. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible.
-Le troisième paramétre (``security_min_on_percent``) est la valeur minimal de ``on_percent`` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament.
+Le troisième paramétre (``safety_min_on_percent``) est la valeur minimal de ``on_percent`` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament.
Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque soit la dernière consigne de chauffage, à l'inverse ``1.00`` ne déclenchera jamais le préréglage sécurité ( ce qui revient à désactiver la fonction).
-Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
+Le quatrième param§tre (``safety_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
-Note: les paramètres `security_min_on_percent` et `security_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
+Note: les paramètres `safety_min_on_percent` et `safety_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
Depuis la version 5.3 il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
```
@@ -702,7 +702,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
> 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
-> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
+> 4. Pour un usage naturel, le ``safety_default_on_percent`` doit être inférieur à ``safety_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
## Le contrôle centralisé
@@ -875,8 +875,8 @@ context:
| ``power_temp`` | Température si délestaqe | X | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X |
-| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
-| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
+| ``safety_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
+| ``safety_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
@@ -902,9 +902,9 @@ context:
- minimal_activation_delay_sec : 300 secondes (à cause du temps de réponse)
## Le capteur de température alimenté par batterie
-- security_delay_min : 60 min (parce que ces capteurs sont paresseux)
-- security_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
-- security_default_on_percent : 0,1 (10% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
+- safety_delay_min : 60 min (parce que ces capteurs sont paresseux)
+- safety_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
+- safety_default_on_percent : 0,1 (10% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
Il faut comprendre ces réglages comme suit :
@@ -917,9 +917,9 @@ Ce qui est important c'est de ne pas prendre trop de risque avec ces paramètres
Versatile Thermostat vous permet d'être notifié lorsqu'un évènement de ce type survient. Mettez en place, les alertes qui vont bien dès l'utilisation de ce thermostat. Cf. (#notifications)
## Capteur de température réactif (sur secteur)
-- security_delay_min : 15 min
-- security_min_on_percent : 0,7 (70% - on passe en preset ``security`` si le radiateur chauffait plus de 70% du temps)
-- security_default_on_percent : 0,25 (25% - en preset ``security``, on garde un fond de chauffe de 25% du temps)
+- safety_delay_min : 15 min
+- safety_min_on_percent : 0,7 (70% - on passe en preset ``security`` si le radiateur chauffait plus de 70% du temps)
+- safety_default_on_percent : 0,25 (25% - en preset ``security``, on garde un fond de chauffe de 25% du temps)
## Mes presets
Ceci est juste un exemple de la façon dont j'utilise le préréglage. A vous de vous adapter à votre configuration mais cela peut être utile pour comprendre son fonctionnement.
@@ -1053,7 +1053,7 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
Pour changer les paramètres de sécurité utilisez le code suivant :
```
-service : versatile_thermostat.set_security
+service : versatile_thermostat.set_safety
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
@@ -1082,7 +1082,7 @@ Les évènements notifiés sont les suivants:
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
-- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes
+- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `safety_delay_min`` minutes
- ``versatile_thermostat_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_central_boiler_event`` : un évènement indiquant un changement dans l'état de la chaudière.
@@ -1126,13 +1126,13 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
| ``window_state`` | Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
-| ``window_bypass_state`` | True si le bypass de la détection d'ouverture et activé |
+| ``is_window_bypass`` | True si le bypass de la détection d'ouverture et activé |
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
-| ``security_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
-| ``security_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
-| ``security_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
+| ``safety_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
+| ``safety_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
+| ``safety_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
| ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne |
| ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
| ``security_state`` | L'état de sécurité. vrai ou faux |
@@ -1571,9 +1571,9 @@ Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les u
Pourquoi mon Versatile Thermostat se met en Securite ?
## Pourquoi mon Versatile Thermostat se met en Securite ?
-Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
+Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `safety_delay_min` minutes et que le radiateur chauffait à au moins `safety_min_on_percent`.
-Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
+Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `safety_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
Tous ces paramètres se règlent dans la dernière page de la configuration du VTherm : "Paramètres avancés".
@@ -1596,14 +1596,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
...
-security_delay_min: 60
+safety_delay_min: 60
```
On voit que :
1. le VTherm est bien en mode sécurité (`security_state: true`),
2. l'heure courante est le 06/12/2023 à 18h43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
3. l'heure de dernière réception de la température intérieure est le 06/12/2023 à 18h43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`). Elle est donc récente,
-4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`security_delay_min: 60`).
+4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`safety_delay_min: 60`).
### Comment être averti lorsque cela se produit ?
Pour être averti, le VTherm envoie un évènement dès que ça se produit et un en fin d'alerte sécurité. Vous pouvez capter ces évènements dans une automatisation et envoyer une notification par exemple, faire clignoter un voyant, déclencher une sirène, ... A vous de voir.
@@ -1613,8 +1613,8 @@ Pour manipuler les évènements générés par le VTherm, cf. [Eveènements](#ev
### Comment réparer ?
Cela va dépendre de la cause du problème :
1. Si un capteur est en défaut, il faut le réparer (remettre des piles, le changer, vérifier l'intégration Météo qui donne la température extérieure, ...),
-2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
-3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
+2. Si le paramètre `safety_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
+3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `safety_delay_min`,
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
diff --git a/documentation/en/reference.md b/documentation/en/reference.md
index e5b2cd1..71fbd50 100644
--- a/documentation/en/reference.md
+++ b/documentation/en/reference.md
@@ -61,8 +61,8 @@
| ``power_temp`` | Temperature during load shedding | X | X | X | X |
| ``presence_sensor_entity_id`` | Presence sensor entity id (true if someone is present) | X | X | X | - |
| ``minimal_activation_delay`` | Minimum activation delay | X | - | - | X |
-| ``security_delay_min`` | Maximum delay between two temperature measurements | X | - | X | X |
-| ``security_min_on_percent`` | Minimum power percentage to enter security mode | X | - | X | X |
+| ``safety_delay_min`` | Maximum delay between two temperature measurements | X | - | X | X |
+| ``safety_min_on_percent`` | Minimum power percentage to enter security mode | X | - | X | X |
| ``auto_regulation_mode`` | Auto-regulation mode | - | X | - | - |
| ``auto_regulation_dtemp`` | Auto-regulation threshold | - | X | - | - |
| ``auto_regulation_period_min`` | Minimum auto-regulation period | - | X | - | - |
@@ -175,7 +175,7 @@ If the thermostat is in ``security`` mode, the new settings are applied immediat
To change the security settings, use the following code:
```yaml
-service: versatile_thermostat.set_security
+service: versatile_thermostat.set_safety
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
@@ -204,7 +204,7 @@ The following events are notified:
- ``versatile_thermostat_security_event``: the thermostat enters or exits the ``security`` preset
- ``versatile_thermostat_power_event``: the thermostat enters or exits the ``power`` preset
-- ``versatile_thermostat_temperature_event``: one or both temperature measurements of the thermostat haven't been updated for more than `security_delay_min`` minutes
+- ``versatile_thermostat_temperature_event``: one or both temperature measurements of the thermostat haven't been updated for more than `safety_delay_min`` minutes
- ``versatile_thermostat_hvac_mode_event``: the thermostat is turned on or off. This event is also broadcast at the thermostat's startup
- ``versatile_thermostat_preset_event``: a new preset is selected on the thermostat. This event is also broadcast at the thermostat's startup
- ``versatile_thermostat_central_boiler_event``: an event indicating a change in the boiler's state
@@ -248,13 +248,13 @@ The custom attributes are as follows:
| ``saved_preset_mode`` | The last preset used before automatic preset switching |
| ``saved_target_temp`` | The last temperature used before automatic switching |
| ``window_state`` | The last known state of the window sensor. None if the window is not configured |
-| ``window_bypass_state`` | True if the window open detection bypass is enabled |
+| ``is_window_bypass`` | True if the window open detection bypass is enabled |
| ``motion_state`` | The last known state of the motion sensor. None if motion detection is not configured |
| ``overpowering_state`` | The last known state of the overpower sensor. None if power management is not configured |
| ``presence_state`` | The last known state of the presence sensor. None if presence detection is not configured |
-| ``security_delay_min`` | The delay before activating security mode when one of the two temperature sensors stops sending measurements |
-| ``security_min_on_percent`` | The heating percentage below which the thermostat will not switch to security |
-| ``security_default_on_percent`` | The heating percentage used when the thermostat is in security mode |
+| ``safety_delay_min`` | The delay before activating security mode when one of the two temperature sensors stops sending measurements |
+| ``safety_min_on_percent`` | The heating percentage below which the thermostat will not switch to security |
+| ``safety_default_on_percent`` | The heating percentage used when the thermostat is in security mode |
| ``last_temperature_datetime`` | The date and time in ISO8866 format of the last internal temperature reception |
| ``last_ext_temperature_datetime`` | The date and time in ISO8866 format of the last external temperature reception |
| ``security_state`` | The security state. True or false |
diff --git a/documentation/en/troubleshooting.md b/documentation/en/troubleshooting.md
index 93a8756..d96e75e 100644
--- a/documentation/en/troubleshooting.md
+++ b/documentation/en/troubleshooting.md
@@ -125,9 +125,9 @@ These parameters are sensitive and quite difficult to adjust. Please only use th
## Why is my Versatile Thermostat going into Safety Mode?
-Safety mode is only available for VTherm types `over_switch` and `over_valve`. It occurs when one of the two thermometers (providing either the room temperature or the external temperature) has not sent a value for more than `security_delay_min` minutes, and the radiator had been heating at least `security_min_on_percent`. See [safety mode](feature-advanced.md#safety-mode)
+Safety mode is only available for VTherm types `over_switch` and `over_valve`. It occurs when one of the two thermometers (providing either the room temperature or the external temperature) has not sent a value for more than `safety_delay_min` minutes, and the radiator had been heating at least `safety_min_on_percent`. See [safety mode](feature-advanced.md#safety-mode)
-Since the algorithm relies on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To prevent this, when the above conditions are detected, heating is limited to the `security_default_on_percent` parameter. This value should therefore be reasonably low (10% is a good value). It helps avoid a fire while preventing the radiator from being completely turned off (risk of freezing).
+Since the algorithm relies on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To prevent this, when the above conditions are detected, heating is limited to the `safety_default_on_percent` parameter. This value should therefore be reasonably low (10% is a good value). It helps avoid a fire while preventing the radiator from being completely turned off (risk of freezing).
All these parameters are configured on the last page of the VTherm configuration: "Advanced Settings".
@@ -151,14 +151,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
...
-security_delay_min: 60
+safety_delay_min: 60
```
We can see that:
1. The VTherm is indeed in safety mode (`security_state: true`),
2. The current time is 06/12/2023 at 18:43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
3. The last reception time of the room temperature is 06/12/2023 at 18:43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`), so it's recent,
-4. The last reception time of the external temperature is 06/12/2023 at 13:04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"`). The external temperature is over 5 hours late, which triggered the safety mode, as the threshold is set to 60 minutes (`security_delay_min: 60`).
+4. The last reception time of the external temperature is 06/12/2023 at 13:04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"`). The external temperature is over 5 hours late, which triggered the safety mode, as the threshold is set to 60 minutes (`safety_delay_min: 60`).
### How to Be Notified When This Happens?
The VTherm sends an event as soon as this happens and again at the end of the safety alert. You can capture these events in an automation and send a notification, blink a light, trigger a siren, etc. It's up to you.
@@ -168,8 +168,8 @@ For handling events generated by VTherm, see [Events](#events).
### How to Fix It?
It depends on the cause of the problem:
1. If a sensor is faulty, it should be repaired (replace batteries, change it, check the weather integration that provides the external temperature, etc.),
-2. If the `security_delay_min` parameter is too small, it may generate many false alerts. A correct value is around 60 minutes, especially if you have battery-powered temperature sensors. See [my settings](tuning-examples.md#battery-powered-temperature-sensor),
-3. Some temperature sensors don't send measurements if the temperature hasn't changed. So if the temperature stays very stable for a long time, safety mode can trigger. This is not a big issue since it will deactivate once the VTherm receives a new temperature. On some thermometers (e.g., TuYA or Zigbee), you can force a max delay between two measurements. The max delay should be set to a value lower than `security_delay_min`,
+2. If the `safety_delay_min` parameter is too small, it may generate many false alerts. A correct value is around 60 minutes, especially if you have battery-powered temperature sensors. See [my settings](tuning-examples.md#battery-powered-temperature-sensor),
+3. Some temperature sensors don't send measurements if the temperature hasn't changed. So if the temperature stays very stable for a long time, safety mode can trigger. This is not a big issue since it will deactivate once the VTherm receives a new temperature. On some thermometers (e.g., TuYA or Zigbee), you can force a max delay between two measurements. The max delay should be set to a value lower than `safety_delay_min`,
4. As soon as the temperature is received again, safety mode will turn off, and the previous preset, target temperature, and mode values will be restored.
5. If the external temperature sensor is faulty, you can disable safety mode triggering as it has a minimal impact on the results. To do so, see [here](feature-advanced.md#safety-mode).
diff --git a/documentation/en/tuning-examples.md b/documentation/en/tuning-examples.md
index c5e2366..3b8eb23 100644
--- a/documentation/en/tuning-examples.md
+++ b/documentation/en/tuning-examples.md
@@ -18,9 +18,9 @@
## Battery-Powered Temperature Sensor
These sensors are often sluggish and do not always send temperature readings when the temperature is stable. Therefore, the settings should be loose to avoid false positives.
-- security_delay_min: 60 minutes (because these sensors are sluggish)
-- security_min_on_percent: 0.7 (70% - the system goes into security mode if the heater was on more than 70% of the time)
-- security_default_on_percent: 0.4 (40% - in security mode, we maintain 40% heating time to avoid getting too cold)
+- safety_delay_min: 60 minutes (because these sensors are sluggish)
+- safety_min_on_percent: 0.7 (70% - the system goes into security mode if the heater was on more than 70% of the time)
+- safety_default_on_percent: 0.4 (40% - in security mode, we maintain 40% heating time to avoid getting too cold)
These settings should be understood as follows:
@@ -35,9 +35,9 @@ Versatile Thermostat allows you to be notified when such an event occurs. Set up
## Reactive Temperature Sensor (plugged in)
A powered thermometer is supposed to be very regular in sending temperature readings. If it doesn't send anything for 15 minutes, it most likely has an issue, and we can react faster without the risk of a false positive.
-- security_delay_min: 15 minutes
-- security_min_on_percent: 0.5 (50% - the system goes into ``security`` preset if the heater was on more than 50% of the time)
-- security_default_on_percent: 0.25 (25% - in ``security`` preset, we keep 25% heating time)
+- safety_delay_min: 15 minutes
+- safety_min_on_percent: 0.5 (50% - the system goes into ``security`` preset if the heater was on more than 50% of the time)
+- safety_default_on_percent: 0.25 (25% - in ``security`` preset, we keep 25% heating time)
## My Presets
This is just an example of how I use the preset. You can adapt it to your configuration, but it may be useful to understand its functionality.
diff --git a/documentation/fr/feature-advanced.md b/documentation/fr/feature-advanced.md
index ef33d13..101bca6 100644
--- a/documentation/fr/feature-advanced.md
+++ b/documentation/fr/feature-advanced.md
@@ -27,12 +27,12 @@ Le premier délai (`minimal_activation_delay_sec`) en secondes est le délai min
### La mise en sécurité
-Le deuxième délai (`security_delay_min`) est le délai maximal entre deux mesures de température avant de passer le _VTherm_ en mode sécurité.
+Le deuxième délai (`safety_delay_min`) est le délai maximal entre deux mesures de température avant de passer le _VTherm_ en mode sécurité.
-Le troisième paramètre (`security_min_on_percent`) est la valeur minimal de `on_percent` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament. En effet, il n'y a pas de risque physique pour le logement dans ce cas mais juste un risque de surchauffe ou de sous-chauffe.
+Le troisième paramètre (`safety_min_on_percent`) est la valeur minimal de `on_percent` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament. En effet, il n'y a pas de risque physique pour le logement dans ce cas mais juste un risque de surchauffe ou de sous-chauffe.
Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque soit la dernière consigne de chauffage, à l'inverse ``1.00`` ne déclenchera jamais le préréglage sécurité ( ce qui revient à désactiver la fonction). Ce peut ê
-Le quatrième paramètre (`security_default_on_percent`) est la valeur de `on_percent` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez `0` alors le thermostat sera coupé lorsqu'il passe en mode `security`, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
+Le quatrième paramètre (`safety_default_on_percent`) est la valeur de `on_percent` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez `0` alors le thermostat sera coupé lorsqu'il passe en mode `security`, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
Il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
```yaml
@@ -47,5 +47,5 @@ Par défaut, le thermomètre extérieur peut déclencher une mise en sécurité
> 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. Une action est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
-> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
+> 4. Pour un usage naturel, le ``safety_default_on_percent`` doit être inférieur à ``safety_min_on_percent``,
> 5. Si vous utilisez la carte Verstatile Thermostat UI (cf. [ici](additions.md#bien-mieux-avec-le-versatile-thermostat-ui-card)), un _Vtherm_ en mode sécurité est signalé par un voile grisatre qui donne le thermomètre en défaut et depuis combien de temps le thermomètre n'a pas remonté de valeur : .
diff --git a/documentation/fr/one-page.md b/documentation/fr/one-page.md
index 9fe2af5..ea7ae99 100644
--- a/documentation/fr/one-page.md
+++ b/documentation/fr/one-page.md
@@ -678,14 +678,14 @@ Le formulaire de configuration avancée est le suivant :
Le premier délai (minimal_activation_delay_sec) en secondes est le délai minimum acceptable pour allumer le chauffage. Lorsque le calcul donne un délai de mise sous tension inférieur à cette valeur, le chauffage reste éteint.
-Le deuxième délai (``security_delay_min``) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security``. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur passeront en mode ``security`` après ce délai. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible.
+Le deuxième délai (``safety_delay_min``) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security``. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur passeront en mode ``security`` après ce délai. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible.
-Le troisième paramétre (``security_min_on_percent``) est la valeur minimal de ``on_percent`` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament.
+Le troisième paramétre (``safety_min_on_percent``) est la valeur minimal de ``on_percent`` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament.
Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque soit la dernière consigne de chauffage, à l'inverse ``1.00`` ne déclenchera jamais le préréglage sécurité ( ce qui revient à désactiver la fonction).
-Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
+Le quatrième param§tre (``safety_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
-Note: les paramètres `security_min_on_percent` et `security_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
+Note: les paramètres `safety_min_on_percent` et `safety_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
Depuis la version 5.3 il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
```
@@ -702,7 +702,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
> 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
-> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
+> 4. Pour un usage naturel, le ``safety_default_on_percent`` doit être inférieur à ``safety_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
## Le contrôle centralisé
@@ -875,8 +875,8 @@ context:
| ``power_temp`` | Température si délestaqe | X | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X |
-| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
-| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
+| ``safety_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
+| ``safety_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
@@ -902,9 +902,9 @@ context:
- minimal_activation_delay_sec : 300 secondes (à cause du temps de réponse)
## Le capteur de température alimenté par batterie
-- security_delay_min : 60 min (parce que ces capteurs sont paresseux)
-- security_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
-- security_default_on_percent : 0,1 (10% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
+- safety_delay_min : 60 min (parce que ces capteurs sont paresseux)
+- safety_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
+- safety_default_on_percent : 0,1 (10% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
Il faut comprendre ces réglages comme suit :
@@ -917,9 +917,9 @@ Ce qui est important c'est de ne pas prendre trop de risque avec ces paramètres
Versatile Thermostat vous permet d'être notifié lorsqu'un évènement de ce type survient. Mettez en place, les alertes qui vont bien dès l'utilisation de ce thermostat. Cf. (#notifications)
## Capteur de température réactif (sur secteur)
-- security_delay_min : 15 min
-- security_min_on_percent : 0,7 (70% - on passe en preset ``security`` si le radiateur chauffait plus de 70% du temps)
-- security_default_on_percent : 0,25 (25% - en preset ``security``, on garde un fond de chauffe de 25% du temps)
+- safety_delay_min : 15 min
+- safety_min_on_percent : 0,7 (70% - on passe en preset ``security`` si le radiateur chauffait plus de 70% du temps)
+- safety_default_on_percent : 0,25 (25% - en preset ``security``, on garde un fond de chauffe de 25% du temps)
## Mes presets
Ceci est juste un exemple de la façon dont j'utilise le préréglage. A vous de vous adapter à votre configuration mais cela peut être utile pour comprendre son fonctionnement.
@@ -1053,7 +1053,7 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
Pour changer les paramètres de sécurité utilisez le code suivant :
```
-service : versatile_thermostat.set_security
+service : versatile_thermostat.set_safety
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
@@ -1082,7 +1082,7 @@ Les évènements notifiés sont les suivants:
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
-- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes
+- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `safety_delay_min`` minutes
- ``versatile_thermostat_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_central_boiler_event`` : un évènement indiquant un changement dans l'état de la chaudière.
@@ -1126,13 +1126,13 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
| ``window_state`` | Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
-| ``window_bypass_state`` | True si le bypass de la détection d'ouverture et activé |
+| ``is_window_bypass`` | True si le bypass de la détection d'ouverture et activé |
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
-| ``security_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
-| ``security_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
-| ``security_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
+| ``safety_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
+| ``safety_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
+| ``safety_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
| ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne |
| ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
| ``security_state`` | L'état de sécurité. vrai ou faux |
@@ -1571,9 +1571,9 @@ Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les u
Pourquoi mon Versatile Thermostat se met en Securite ?
## Pourquoi mon Versatile Thermostat se met en Securite ?
-Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
+Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `safety_delay_min` minutes et que le radiateur chauffait à au moins `safety_min_on_percent`.
-Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
+Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `safety_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
Tous ces paramètres se règlent dans la dernière page de la configuration du VTherm : "Paramètres avancés".
@@ -1596,14 +1596,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
...
-security_delay_min: 60
+safety_delay_min: 60
```
On voit que :
1. le VTherm est bien en mode sécurité (`security_state: true`),
2. l'heure courante est le 06/12/2023 à 18h43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
3. l'heure de dernière réception de la température intérieure est le 06/12/2023 à 18h43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`). Elle est donc récente,
-4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`security_delay_min: 60`).
+4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`safety_delay_min: 60`).
### Comment être averti lorsque cela se produit ?
Pour être averti, le VTherm envoie un évènement dès que ça se produit et un en fin d'alerte sécurité. Vous pouvez capter ces évènements dans une automatisation et envoyer une notification par exemple, faire clignoter un voyant, déclencher une sirène, ... A vous de voir.
@@ -1613,8 +1613,8 @@ Pour manipuler les évènements générés par le VTherm, cf. [Eveènements](#ev
### Comment réparer ?
Cela va dépendre de la cause du problème :
1. Si un capteur est en défaut, il faut le réparer (remettre des piles, le changer, vérifier l'intégration Météo qui donne la température extérieure, ...),
-2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
-3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
+2. Si le paramètre `safety_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
+3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `safety_delay_min`,
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
diff --git a/documentation/fr/reference.md b/documentation/fr/reference.md
index 0802a6d..820acd5 100644
--- a/documentation/fr/reference.md
+++ b/documentation/fr/reference.md
@@ -61,8 +61,8 @@
| ``power_temp`` | Température si délestaqe | X | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X |
-| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
-| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
+| ``safety_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
+| ``safety_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
@@ -173,7 +173,7 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
Pour changer les paramètres de sécurité utilisez le code suivant :
```yaml
-service : versatile_thermostat.set_security
+service : versatile_thermostat.set_safety
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
@@ -202,7 +202,7 @@ Les évènements notifiés sont les suivants:
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
-- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes
+- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `safety_delay_min`` minutes
- ``versatile_thermostat_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_central_boiler_event`` : un évènement indiquant un changement dans l'état de la chaudière.
@@ -247,13 +247,13 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
| ``window_state`` | Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
-| ``window_bypass_state`` | True si le bypass de la détection d'ouverture et activé |
+| ``is_window_bypass`` | True si le bypass de la détection d'ouverture et activé |
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
-| ``security_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
-| ``security_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
-| ``security_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
+| ``safety_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
+| ``safety_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
+| ``safety_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
| ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne |
| ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
| ``security_state`` | L'état de sécurité. vrai ou faux |
diff --git a/documentation/fr/troubleshooting.md b/documentation/fr/troubleshooting.md
index 6a24992..8eea2f7 100644
--- a/documentation/fr/troubleshooting.md
+++ b/documentation/fr/troubleshooting.md
@@ -123,9 +123,9 @@ versatile_thermostat:
Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les utiliser que si vous savez ce que vous faites et que vos mesures de température ne sont pas déjà lisses.
## Pourquoi mon Versatile Thermostat se met en Securite ?
-Le mode sécurité est possible sur les types de VTherm de type `over_switch` et `over_valve` uniquement. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`. Cf. [mode sécurité](feature-advanced.md#la-mise-en-sécurité)
+Le mode sécurité est possible sur les types de VTherm de type `over_switch` et `over_valve` uniquement. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `safety_delay_min` minutes et que le radiateur chauffait à au moins `safety_min_on_percent`. Cf. [mode sécurité](feature-advanced.md#la-mise-en-sécurité)
-Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
+Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `safety_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
Tous ces paramètres se règlent dans la dernière page de la configuration du VTherm : "Paramètres avancés".
@@ -149,14 +149,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
...
-security_delay_min: 60
+safety_delay_min: 60
```
On voit que :
1. le VTherm est bien en mode sécurité (`security_state: true`),
2. l'heure courante est le 06/12/2023 à 18h43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
3. l'heure de dernière réception de la température intérieure est le 06/12/2023 à 18h43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`). Elle est donc récente,
-4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`security_delay_min: 60`).
+4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`safety_delay_min: 60`).
### Comment être averti lorsque cela se produit ?
Pour être averti, le VTherm envoie un évènement dès que ça se produit et un en fin d'alerte sécurité. Vous pouvez capter ces évènements dans une automatisation et envoyer une notification par exemple, faire clignoter un voyant, déclencher une sirène, ... A vous de voir.
@@ -166,8 +166,8 @@ Pour manipuler les évènements générés par le VTherm, cf. [Eveènements](#ev
### Comment réparer ?
Cela va dépendre de la cause du problème :
1. Si un capteur est en défaut, il faut le réparer (remettre des piles, le changer, vérifier l'intégration Météo qui donne la température extérieure, ...),
-2. Si le paramètre `security_delay_min` est trop petit, cela risque de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile. Cf [mes réglages](tuning-examples.md#le-capteur-de-température-alimenté-par-batterie)
-3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple ou Zigbee), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
+2. Si le paramètre `safety_delay_min` est trop petit, cela risque de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile. Cf [mes réglages](tuning-examples.md#le-capteur-de-température-alimenté-par-batterie)
+3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple ou Zigbee), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `safety_delay_min`,
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
5. Si c'est le capteur de température extérieur qui est en défaut, vous pouvez désactiver le déclenchement du mode sécurité puisqu'il influe assez peu sur le résultat. Pour ce faire, cf. [ici](feature-advanced.md#la-mise-en-sécurité)
diff --git a/documentation/fr/tuning-examples.md b/documentation/fr/tuning-examples.md
index 40d3f07..3a4f810 100644
--- a/documentation/fr/tuning-examples.md
+++ b/documentation/fr/tuning-examples.md
@@ -18,9 +18,9 @@
## Le capteur de température alimenté par batterie
Ces capteurs sont souvent paresseux et n'envoit pas toujours de mesure de température lorsqu'elle est stable. Par conséquent, les réglages doivent être laches pour éviter les faux positifs.
-- security_delay_min : 60 min (parce que ces capteurs sont paresseux)
-- security_min_on_percent : 0,7 (70% - on passe en mode sécurité si le radiateur chauffait plus de 70% du temps)
-- security_default_on_percent : 0,4 (40% - en mode sécurité, on garde un fond de chauffe de 40% du temps pour éviter d'avoir trop froid)
+- safety_delay_min : 60 min (parce que ces capteurs sont paresseux)
+- safety_min_on_percent : 0,7 (70% - on passe en mode sécurité si le radiateur chauffait plus de 70% du temps)
+- safety_default_on_percent : 0,4 (40% - en mode sécurité, on garde un fond de chauffe de 40% du temps pour éviter d'avoir trop froid)
Il faut comprendre ces réglages comme suit :
@@ -34,9 +34,9 @@ Versatile Thermostat vous permet d'être notifié lorsqu'un évènement de ce ty
## Capteur de température réactif (sur secteur)
Un thermomètre alimenté est censé est très régulier dans l'envoi des températures. Si il n'envoie rien pendant 15 min, il a certainement un soucis et on peut réagir plus vite sans risque de faux positif.
-- security_delay_min : 15 min
-- security_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
-- security_default_on_percent : 0,25 (20% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
+- safety_delay_min : 15 min
+- safety_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
+- safety_default_on_percent : 0,25 (20% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
## Mes presets
diff --git a/faq.md b/faq.md
index 058cc9d..310fe0c 100644
--- a/faq.md
+++ b/faq.md
@@ -153,9 +153,9 @@ Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les u
Pourquoi mon Versatile Thermostat se met en Securite ?
## Pourquoi mon Versatile Thermostat se met en Securite ?
-Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
+Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `safety_delay_min` minutes et que le radiateur chauffait à au moins `safety_min_on_percent`.
-Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
+Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `safety_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
Tous ces paramètres se règlent dans la dernière page de la configuration du VTherm : "Paramètres avancés".
@@ -178,14 +178,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
...
-security_delay_min: 60
+safety_delay_min: 60
```
On voit que :
1. le VTherm est bien en mode sécurité (`security_state: true`),
2. l'heure courante est le 06/12/2023 à 18h43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
3. l'heure de dernière réception de la température intérieure est le 06/12/2023 à 18h43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`). Elle est donc récente,
-4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`security_delay_min: 60`).
+4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`safety_delay_min: 60`).
### Comment être averti lorsque cela se produit ?
Pour être averti, le VTherm envoie un évènement dès que ça se produit et un en fin d'alerte sécurité. Vous pouvez capter ces évènements dans une automatisation et envoyer une notification par exemple, faire clignoter un voyant, déclencher une sirène, ... A vous de voir.
@@ -195,8 +195,8 @@ Pour manipuler les évènements générés par le VTherm, cf. [Eveènements](#ev
### Comment réparer ?
Cela va dépendre de la cause du problème :
1. Si un capteur est en défaut, il faut le réparer (remettre des piles, le changer, vérifier l'intégration Météo qui donne la température extérieure, ...),
-2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
-3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
+2. Si le paramètre `safety_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
+3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `safety_delay_min`,
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
diff --git a/hacs.json b/hacs.json
index 75d218f..e96916f 100644
--- a/hacs.json
+++ b/hacs.json
@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
- "homeassistant": "2024.12.3"
+ "homeassistant": "2024.12.4"
}
\ No newline at end of file
diff --git a/tests/commons.py b/tests/commons.py
index 7477036..1b12423 100644
--- a/tests/commons.py
+++ b/tests/commons.py
@@ -1,4 +1,4 @@
-# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
+# pylint: disable=wildcard-import, unused-wildcard-import, unused-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
""" Some common resources """
import asyncio
@@ -8,7 +8,16 @@ from unittest.mock import patch, MagicMock # pylint: disable=unused-import
import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
-from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
+from homeassistant.const import (
+ UnitOfTemperature,
+ STATE_ON,
+ STATE_OFF,
+ ATTR_TEMPERATURE,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+ STATE_HOME,
+ STATE_NOT_HOME,
+)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity import Entity
@@ -188,9 +197,9 @@ FULL_CENTRAL_CONFIG = {
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
}
@@ -229,9 +238,9 @@ FULL_CENTRAL_CONFIG_WITH_BOILER = {
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
@@ -590,12 +599,7 @@ async def create_thermostat(
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
- # We should reload the VTherm links
- # vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
- # central_config = vtherm_api.find_central_configuration()
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
- # if entity and hasattr(entity, "init_presets")::
- # await entity.init_presets(central_config)
return entity
@@ -737,7 +741,7 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
)
},
)
- await entity._async_power_changed(power_event)
+ await entity.power_manager._async_power_sensor_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
@@ -763,7 +767,7 @@ async def send_max_power_change_event(
)
},
)
- await entity._async_max_power_changed(power_event)
+ await entity.power_manager._async_max_power_sensor_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
@@ -796,7 +800,7 @@ async def send_window_change_event(
),
},
)
- ret = await entity._async_windows_changed(window_event)
+ ret = await entity.window_manager._window_sensor_changed(window_event)
if sleep:
await asyncio.sleep(0.1)
return ret
@@ -830,14 +834,14 @@ async def send_motion_change_event(
),
},
)
- ret = await entity._async_motion_changed(motion_event)
+ ret = await entity.motion_manager._motion_sensor_changed(motion_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_presence_change_event(
- entity: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
+ vtherm: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new presence event simulating a change on the window state"""
_LOGGER.info(
@@ -845,26 +849,26 @@ async def send_presence_change_event(
new_state,
old_state,
date,
- entity,
+ vtherm,
)
presence_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
- entity_id=entity.entity_id,
+ entity_id=vtherm.entity_id,
state=STATE_ON if new_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
"old_state": State(
- entity_id=entity.entity_id,
+ entity_id=vtherm.entity_id,
state=STATE_ON if old_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
},
)
- ret = await entity._async_presence_changed(presence_event)
+ ret = await vtherm._presence_manager._presence_sensor_changed(presence_event)
if sleep:
await asyncio.sleep(0.1)
return ret
@@ -1000,7 +1004,7 @@ async def set_climate_preset_temp(
await temp_entity.async_set_native_value(temp)
else:
_LOGGER.warning(
- "commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
+ "commons tests set_climate_preset_temp: cannot find number entity with entity_id '%s'",
number_entity_id,
)
@@ -1062,9 +1066,14 @@ async def set_all_climate_preset_temp(
NUMBER_DOMAIN,
)
assert temp_entity
+ if not temp_entity:
+ raise ConfigurationNotCompleteError(
+ f"'{number_entity_name}' don't exists as number entity"
+ )
# Because set_value is not implemented in Number class (really don't understand why...)
assert temp_entity.state == value
+ await hass.async_block_till_done()
#
# Side effects management
diff --git a/tests/const.py b/tests/const.py
index 5214c22..0940ddd 100644
--- a/tests/const.py
+++ b/tests/const.py
@@ -89,7 +89,12 @@ MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
}
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
- CONF_UNDERLYING_LIST: ["switch.mock_4switch0", "switch.mock_4switch1","switch.mock_4switch2","switch.mock_4switch3"],
+ CONF_UNDERLYING_LIST: [
+ "switch.mock_4switch0",
+ "switch.mock_4switch1",
+ "switch.mock_4switch2",
+ "switch.mock_4switch3",
+ ],
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
@@ -195,9 +200,9 @@ MOCK_PRESENCE_AC_CONFIG = {
MOCK_ADVANCED_CONFIG = {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
}
MOCK_DEFAULT_FEATURE_CONFIG = {
diff --git a/tests/test_auto_fan_mode.py b/tests/test_auto_fan_mode.py
index 768ed33..a7271b5 100644
--- a/tests/test_auto_fan_mode.py
+++ b/tests/test_auto_fan_mode.py
@@ -53,8 +53,8 @@ async def test_over_climate_auto_fan_mode_turbo(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
},
)
@@ -119,8 +119,8 @@ async def test_over_climate_auto_fan_mode_not_turbo(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
},
)
@@ -189,8 +189,8 @@ async def test_over_climate_auto_fan_mode_turbo_activation(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
diff --git a/tests/test_auto_start_stop.py b/tests/test_auto_start_stop.py
index 212890b..62ede37 100644
--- a/tests/test_auto_start_stop.py
+++ b/tests/test_auto_start_stop.py
@@ -1,4 +1,4 @@
-# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
+# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable, too-many-lines
""" Test the Auto Start Stop algorithm management """
from datetime import datetime, timedelta
@@ -335,8 +335,8 @@ async def test_auto_start_stop_none_vtherm(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
@@ -363,15 +363,14 @@ async def test_auto_start_stop_none_vtherm(
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
- assert (
- vtherm._attr_extra_state_attributes["auto_start_stop_level"]
- == AUTO_START_STOP_LEVEL_NONE
- )
-
- assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] is None
+ assert vtherm._attr_extra_state_attributes.get("auto_start_stop_level") is None
+ assert vtherm._attr_extra_state_attributes.get("auto_start_stop_dtmin") is None
# 1. Vtherm auto-start/stop should be in NONE mode
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_NONE
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_NONE
+ )
# 2. We should not find any switch Enable entity
assert (
@@ -427,8 +426,8 @@ async def test_auto_start_stop_medium_heat_vtherm(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_MEDIUM,
@@ -464,7 +463,10 @@ async def test_auto_start_stop_medium_heat_vtherm(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 15
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_MEDIUM
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_MEDIUM
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
@@ -488,7 +490,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
# 3. Set current temperature to 19 5 min later
now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -500,7 +502,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
assert vtherm.hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 0
assert (
- vtherm._auto_start_stop_algo.accumulated_error == 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 0
) # target = current = 19
# 4. Set current temperature to 20 5 min later
@@ -516,7 +518,10 @@ async def test_auto_start_stop_medium_heat_vtherm(
assert vtherm.hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 0
# accumulated_error = target - current = -1 x 5 min / 2
- assert vtherm._auto_start_stop_algo.accumulated_error == -2.5
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error
+ == -2.5
+ )
# 5. Set current temperature to 21 5 min later -> should turn off
now = now + timedelta(minutes=5)
@@ -532,7 +537,9 @@ async def test_auto_start_stop_medium_heat_vtherm(
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
# accumulated_error = -2.5 + target - current = -2 x 5 min / 2 capped to 5
- assert vtherm._auto_start_stop_algo.accumulated_error == -5
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -5
+ )
# a message should have been sent
assert mock_send_event.call_count >= 1
@@ -577,7 +584,9 @@ async def test_auto_start_stop_medium_heat_vtherm(
await hass.async_block_till_done()
# accumulated_error = .... capped to -5
- assert vtherm._auto_start_stop_algo.accumulated_error == -5
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -5
+ )
# VTherm should stay stopped cause slope is too low to allow the turn to On
assert vtherm.hvac_mode == HVACMode.OFF
@@ -593,7 +602,9 @@ async def test_auto_start_stop_medium_heat_vtherm(
await hass.async_block_till_done()
# accumulated_error = -5/2 + target - current = 1 x 20 min / 2 capped to 5
- assert vtherm._auto_start_stop_algo.accumulated_error == 5
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 5
+ )
# VTherm should have been stopped
assert vtherm.hvac_mode == HVACMode.HEAT
@@ -680,8 +691,8 @@ async def test_auto_start_stop_fast_ac_vtherm(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -717,7 +728,10 @@ async def test_auto_start_stop_fast_ac_vtherm(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in MEDIUM mode
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
@@ -736,7 +750,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
# 3. Set current temperature to 19 5 min later
now = now + timedelta(minutes=5)
# reset accumulated error for test
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -748,7 +762,8 @@ async def test_auto_start_stop_fast_ac_vtherm(
assert vtherm.hvac_mode == HVACMode.COOL
assert mock_send_event.call_count == 0
assert (
- vtherm._auto_start_stop_algo.accumulated_error == 0 # target = current = 25
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error
+ == 0 # target = current = 25
)
# 4. Set current temperature to 23 5 min later -> should turn off
@@ -764,7 +779,9 @@ async def test_auto_start_stop_fast_ac_vtherm(
assert vtherm.hvac_mode == HVACMode.OFF
# accumulated_error = target - current = 2 x 5 min / 2 capped to 2
- assert vtherm._auto_start_stop_algo.accumulated_error == 2
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 2
+ )
# a message should have been sent
assert mock_send_event.call_count >= 1
@@ -809,7 +826,9 @@ async def test_auto_start_stop_fast_ac_vtherm(
await hass.async_block_till_done()
# accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2
- assert vtherm._auto_start_stop_algo.accumulated_error == -2
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2
+ )
# VTherm should stay stopped
assert vtherm.hvac_mode == HVACMode.OFF
@@ -826,7 +845,9 @@ async def test_auto_start_stop_fast_ac_vtherm(
await hass.async_block_till_done()
# accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2
- assert vtherm._auto_start_stop_algo.accumulated_error == -2
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2
+ )
# VTherm should have been stopped
assert vtherm.hvac_mode == HVACMode.COOL
@@ -911,8 +932,8 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -948,7 +969,10 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in MEDIUM mode
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
@@ -966,7 +990,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
# 3. Set current temperature to 21 5 min later to auto-stop
now = now + timedelta(minutes=5)
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -977,7 +1001,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
# VTherm should have been stopped
assert vtherm.hvac_mode == HVACMode.OFF
- assert vtherm._auto_start_stop_algo.accumulated_error == -2
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2
+ )
# a message should have been sent
assert mock_send_event.call_count >= 1
@@ -1032,7 +1058,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
await hass.async_block_till_done()
assert vtherm.target_temperature == 21
- assert vtherm._auto_start_stop_algo.accumulated_error == 2
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 2
+ )
# VTherm should have been restarted
assert vtherm.hvac_mode == HVACMode.HEAT
@@ -1117,8 +1145,8 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -1154,7 +1182,10 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in FAST mode and enable should be on
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
@@ -1185,7 +1216,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
# 3. Set current temperature to 21 5 min later to auto-stop
now = now + timedelta(minutes=5)
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -1197,7 +1228,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
assert vtherm.hvac_mode == HVACMode.HEAT
# Not calculated cause enable = false
- assert vtherm._auto_start_stop_algo.accumulated_error == 0
+ assert (
+ vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 0
+ )
# a message should have been sent
assert mock_send_event.call_count == 0
@@ -1251,8 +1284,8 @@ async def test_auto_start_stop_fast_heat_window(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -1288,7 +1321,10 @@ async def test_auto_start_stop_fast_heat_window(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
@@ -1310,12 +1346,12 @@ async def test_auto_start_stop_fast_heat_window(
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
- assert vtherm.window_state == STATE_OFF
+ assert vtherm.window_state == STATE_UNKNOWN # cause condition is not evaluated
# 3. Set current temperature to 21 5 min later -> should turn off VTherm
now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -1426,8 +1462,8 @@ async def test_auto_start_stop_fast_heat_window_mixed(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -1463,7 +1499,10 @@ async def test_auto_start_stop_fast_heat_window_mixed(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
@@ -1485,7 +1524,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
- assert vtherm.window_state == STATE_OFF
+ assert vtherm.window_state == STATE_UNKNOWN # cause try_condition is not evaluated
# 3. Open the window and wait for the delay
now = now + timedelta(minutes=2)
@@ -1513,7 +1552,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
# 4. Set current temperature to 21 5 min later -> should turn off VTherm
now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -1605,8 +1644,8 @@ async def test_auto_start_stop_disable_vtherm_off(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: False,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -1637,6 +1676,9 @@ async def test_auto_start_stop_disable_vtherm_off(
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
+ assert (
+ vtherm._attr_extra_state_attributes["is_auto_start_stop_configured"] is True
+ )
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
@@ -1646,7 +1688,10 @@ async def test_auto_start_stop_disable_vtherm_off(
# 1. Vtherm auto-start/stop should be in FAST mode and enable should be on
vtherm._set_now(now)
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
diff --git a/tests/test_auto_start_stop_feature_manager.py b/tests/test_auto_start_stop_feature_manager.py
new file mode 100644
index 0000000..8fe17ad
--- /dev/null
+++ b/tests/test_auto_start_stop_feature_manager.py
@@ -0,0 +1,120 @@
+# pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines
+""" Test the Window management """
+import logging
+from unittest.mock import PropertyMock, MagicMock
+
+from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
+
+from custom_components.versatile_thermostat.feature_auto_start_stop_manager import (
+ FeatureAutoStartStopManager,
+)
+from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
+
+logging.getLogger().setLevel(logging.DEBUG)
+
+
+async def test_auto_start_stop_feature_manager_create(
+ hass: HomeAssistant,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ auto_start_stop_manager = FeatureAutoStartStopManager(fake_vtherm, hass)
+
+ assert auto_start_stop_manager is not None
+ assert auto_start_stop_manager.is_configured is False
+ assert auto_start_stop_manager.is_auto_stopped is False
+ assert auto_start_stop_manager.auto_start_stop_enable is False
+ assert auto_start_stop_manager.name == "the name"
+
+ assert len(auto_start_stop_manager._active_listener) == 0
+
+ custom_attributes = {}
+ auto_start_stop_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["is_auto_start_stop_configured"] is False
+ # assert custom_attributes["auto_start_stop_enable"] is False
+ # assert custom_attributes["auto_start_stop_level"] == AUTO_START_STOP_LEVEL_NONE
+ # assert custom_attributes["auto_start_stop_dtmin"] is None
+ # assert custom_attributes["auto_start_stop_accumulated_error"] is None
+ # assert custom_attributes["auto_start_stop_accumulated_error_threshold"] is None
+ # assert custom_attributes["auto_start_stop_last_switch_date"] is None
+
+
+@pytest.mark.parametrize(
+ "use_auto_start_stop_feature, level, is_configured",
+ [
+ # fmt: off
+ ( True, AUTO_START_STOP_LEVEL_NONE, True),
+ ( True, AUTO_START_STOP_LEVEL_SLOW, True),
+ ( True, AUTO_START_STOP_LEVEL_MEDIUM, True),
+ ( True, AUTO_START_STOP_LEVEL_FAST, True),
+ # Level is missing , will be set to None
+ ( True, None, True),
+ ( False, AUTO_START_STOP_LEVEL_NONE, False),
+ ( False, AUTO_START_STOP_LEVEL_SLOW, False),
+ ( False, AUTO_START_STOP_LEVEL_MEDIUM, False),
+ ( False, AUTO_START_STOP_LEVEL_FAST, False),
+ # Level is missing , will be set to None
+ ( False, None, False),
+ # fmt: on
+ ],
+)
+async def test_auto_start_stop_feature_manager_post_init(
+ hass: HomeAssistant, use_auto_start_stop_feature, level, is_configured
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ auto_start_stop_manager = FeatureAutoStartStopManager(fake_vtherm, hass)
+ assert auto_start_stop_manager is not None
+
+ # 2. post_init
+ auto_start_stop_manager.post_init(
+ {
+ CONF_USE_AUTO_START_STOP_FEATURE: use_auto_start_stop_feature,
+ CONF_AUTO_START_STOP_LEVEL: level,
+ }
+ )
+
+ assert auto_start_stop_manager.is_configured is is_configured
+ assert (
+ auto_start_stop_manager.auto_start_stop_level == level
+ if level and is_configured
+ else AUTO_START_STOP_LEVEL_NONE
+ )
+ assert auto_start_stop_manager.auto_start_stop_enable is False
+ assert auto_start_stop_manager._auto_start_stop_algo is not None
+
+ custom_attributes = {}
+ auto_start_stop_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["is_auto_start_stop_configured"] is is_configured
+
+ if auto_start_stop_manager.is_configured:
+ assert custom_attributes["auto_start_stop_enable"] is False
+ assert (
+ custom_attributes["auto_start_stop_level"] == level
+ if level and is_configured
+ else AUTO_START_STOP_LEVEL_NONE
+ )
+ assert (
+ custom_attributes["auto_start_stop_dtmin"]
+ == auto_start_stop_manager._auto_start_stop_algo.dt_min
+ )
+ assert (
+ custom_attributes["auto_start_stop_accumulated_error"]
+ == auto_start_stop_manager._auto_start_stop_algo.accumulated_error
+ )
+ assert (
+ custom_attributes["auto_start_stop_accumulated_error_threshold"]
+ == auto_start_stop_manager._auto_start_stop_algo.accumulated_error_threshold
+ )
+ assert (
+ custom_attributes["auto_start_stop_last_switch_date"]
+ == auto_start_stop_manager._auto_start_stop_algo.last_switch_date
+ )
diff --git a/tests/test_binary_sensors.py b/tests/test_binary_sensors.py
index 64852f7..d2c3f15 100644
--- a/tests/test_binary_sensors.py
+++ b/tests/test_binary_sensors.py
@@ -57,8 +57,8 @@ async def test_security_binary_sensors(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
@@ -84,17 +84,17 @@ async def test_security_binary_sensors(
# Set temperature in the past
event_timestamp = now - timedelta(minutes=6)
- # set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
+ # set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
- assert entity.security_state is True
+ assert entity.safety_state is STATE_ON
# Simulate the event reception
await security_binary_sensor.async_my_climate_changed()
assert security_binary_sensor.state == STATE_ON
# set temperature now
await send_temperature_change_event(entity, 15, now)
- assert entity.security_state is False
+ assert entity.safety_state is not STATE_ON
# Simulate the event reception
await security_binary_sensor.async_my_climate_changed()
assert security_binary_sensor.state == STATE_OFF
@@ -134,8 +134,8 @@ async def test_overpowering_binary_sensors(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
@@ -159,8 +159,8 @@ async def test_overpowering_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
- assert await entity.check_overpowering() is False
- assert entity.overpowering_state is None
+ assert await entity.power_manager.check_overpowering() is False
+ assert entity.power_manager.overpowering_state is STATE_UNKNOWN
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state is STATE_OFF
@@ -168,8 +168,8 @@ async def test_overpowering_binary_sensors(
await send_power_change_event(entity, 100, now)
await send_max_power_change_event(entity, 150, now)
- assert await entity.check_overpowering() is True
- assert entity.overpowering_state is True
+ assert await entity.power_manager.check_overpowering() is True
+ assert entity.power_manager.overpowering_state is STATE_ON
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
@@ -177,8 +177,8 @@ async def test_overpowering_binary_sensors(
# set max power to a low value
await send_max_power_change_event(entity, 201, now)
- assert await entity.check_overpowering() is False
- assert entity.overpowering_state is False
+ assert await entity.power_manager.check_overpowering() is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state == STATE_OFF
@@ -218,8 +218,8 @@ async def test_window_binary_sensors(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
@@ -241,7 +241,7 @@ async def test_window_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state is STATE_OFF
@@ -306,10 +306,12 @@ async def test_motion_binary_sensors(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
+ CONF_MOTION_PRESET: PRESET_BOOST,
+ CONF_NO_MOTION_PRESET: PRESET_ECO,
},
)
@@ -329,7 +331,7 @@ async def test_motion_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
- assert entity.motion_state is None
+ assert entity.motion_state is STATE_UNKNOWN
await motion_binary_sensor.async_my_climate_changed()
assert motion_binary_sensor.state is STATE_OFF
@@ -397,8 +399,8 @@ async def test_presence_binary_sensors(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
@@ -419,7 +421,7 @@ async def test_presence_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
- assert entity.presence_state is None
+ assert entity.presence_state is STATE_UNKNOWN
await presence_binary_sensor.async_my_climate_changed()
assert presence_binary_sensor.state is STATE_OFF
@@ -480,8 +482,8 @@ async def test_binary_sensors_over_climate_minimal(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
diff --git a/tests/test_bugs.py b/tests/test_bugs.py
index 8389a81..e19ba34 100644
--- a/tests/test_bugs.py
+++ b/tests/test_bugs.py
@@ -37,7 +37,7 @@ async def test_bug_63(
skip_turn_on_off_heater,
skip_send_event,
):
- """Test that it should be possible to set the security_default_on_percent to 0"""
+ """Test that it should be possible to set the safety_default_on_percent to 0"""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -63,9 +63,9 @@ async def test_bug_63(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.0, # !! here
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.0, # !! here
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.0, # !! here
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.0, # !! here
CONF_DEVICE_POWER: 200,
},
)
@@ -75,8 +75,8 @@ async def test_bug_63(
)
assert entity
- assert entity._security_min_on_percent == 0
- assert entity._security_default_on_percent == 0
+ assert entity.safety_manager.safety_min_on_percent == 0
+ assert entity.safety_manager.safety_default_on_percent == 0
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
@@ -89,7 +89,7 @@ async def test_bug_64(
skip_turn_on_off_heater,
skip_send_event,
):
- """Test that it should be possible to set the security_default_on_percent to 0"""
+ """Test that it should be possible to set the safety_default_on_percent to 0"""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -115,9 +115,9 @@ async def test_bug_64(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1, # !! here
CONF_DEVICE_POWER: 200,
},
)
@@ -299,8 +299,8 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
@@ -334,7 +334,7 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 18
# waits that the heater starts
await asyncio.sleep(0.1)
@@ -346,10 +346,10 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
# Send power mesurement (theheater is already in the power measurement)
await send_power_change_event(entity, 100, datetime.now())
# No overpowering yet
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_COMFORT
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.is_device_active is True
# 2. An already active heater that switch preset will not switch to overpowering
@@ -365,10 +365,10 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
# waits that the heater starts
await asyncio.sleep(0.1)
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1
@@ -385,10 +385,10 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
# waits that the heater starts
await asyncio.sleep(0.1)
- assert await entity.check_overpowering() is True
+ assert await entity.power_manager.check_overpowering() is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_POWER
- assert entity.overpowering_state is True
+ assert entity.power_manager.overpowering_state is STATE_ON
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -510,8 +510,8 @@ async def test_bug_465(hass: HomeAssistant, skip_hass_states_is_state):
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
diff --git a/tests/test_central_boiler.py b/tests/test_central_boiler.py
index 4f66432..4ccb0a5 100644
--- a/tests/test_central_boiler.py
+++ b/tests/test_central_boiler.py
@@ -111,9 +111,9 @@ async def test_update_central_boiler_state_simple(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
@@ -298,9 +298,9 @@ async def test_update_central_boiler_state_multiple(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
@@ -626,9 +626,9 @@ async def test_update_central_boiler_state_simple_valve(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
@@ -800,9 +800,9 @@ async def test_update_central_boiler_state_simple_climate(
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [climate1.entity_id],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
@@ -990,9 +990,9 @@ async def test_update_central_boiler_state_simple_climate_valve_regulation(
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
@@ -1255,9 +1255,9 @@ async def test_bug_339(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
diff --git a/tests/test_central_config.py b/tests/test_central_config.py
index b74c51b..11cf3bb 100644
--- a/tests/test_central_config.py
+++ b/tests/test_central_config.py
@@ -67,9 +67,9 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
},
)
@@ -135,9 +135,9 @@ async def test_minimal_over_switch_wo_central_config(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
# CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
@@ -167,16 +167,16 @@ async def test_minimal_over_switch_wo_central_config(
assert entity.max_temp == 18
assert entity.target_temperature_step == 0.3
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
- assert entity.is_window_auto_enabled is False
+ assert entity.window_manager.is_window_auto_configured is False
assert entity.nb_underlying_entities == 1
assert entity.underlying_entity_id(0) == "switch.mock_switch"
assert entity.proportional_algorithm is not None
assert entity.proportional_algorithm._tpi_coef_int == 0.3
assert entity.proportional_algorithm._tpi_coef_ext == 0.01
assert entity.proportional_algorithm._minimal_activation_delay == 30
- assert entity._security_delay_min == 5
- assert entity._security_min_on_percent == 0.3
- assert entity._security_default_on_percent == 0.1
+ assert entity.safety_manager.safety_delay_min == 5
+ assert entity.safety_manager.safety_min_on_percent == 0.3
+ assert entity.safety_manager.safety_default_on_percent == 0.1
assert entity.is_inversed
entity.remove_thermostat()
@@ -220,9 +220,9 @@ async def test_full_over_switch_wo_central_config(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 30,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
@@ -274,28 +274,42 @@ async def test_full_over_switch_wo_central_config(
assert entity.proportional_algorithm._tpi_coef_int == 0.3
assert entity.proportional_algorithm._tpi_coef_ext == 0.01
assert entity.proportional_algorithm._minimal_activation_delay == 30
- assert entity._security_delay_min == 5
- assert entity._security_min_on_percent == 0.3
- assert entity._security_default_on_percent == 0.1
+ assert entity.safety_manager.safety_delay_min == 5
+ assert entity.safety_manager.safety_min_on_percent == 0.3
+ assert entity.safety_manager.safety_default_on_percent == 0.1
assert entity.is_inversed is False
- assert entity.is_window_auto_enabled is False # we have an entity_id
- assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
- assert entity._window_delay_sec == 30
- assert entity._window_auto_close_threshold == 0.1
- assert entity._window_auto_open_threshold == 3
- assert entity._window_auto_max_duration == 5
+ assert (
+ entity.window_manager.is_window_auto_configured is False
+ ) # we have an entity_id
+ assert (
+ entity.window_manager._window_sensor_entity_id
+ == "binary_sensor.mock_window_sensor"
+ )
+ assert entity.window_manager.window_delay_sec == 30
+ assert entity.window_manager.window_auto_close_threshold == 0.1
+ assert entity.window_manager.window_auto_open_threshold == 3
+ assert entity.window_manager.window_auto_max_duration == 5
- assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
- assert entity._motion_delay_sec == 10
- assert entity._motion_off_delay_sec == 29
- assert entity._motion_preset == "comfort"
- assert entity._no_motion_preset == "eco"
+ assert (
+ entity.motion_manager.motion_sensor_entity_id
+ == "binary_sensor.mock_motion_sensor"
+ )
+ assert entity.motion_manager.motion_delay_sec == 10
+ assert entity.motion_manager.motion_off_delay_sec == 29
+ assert entity.motion_manager.motion_preset == "comfort"
+ assert entity.motion_manager.no_motion_preset == "eco"
- assert entity._power_sensor_entity_id == "sensor.mock_power_sensor"
- assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
+ assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
+ assert (
+ entity.power_manager.max_power_sensor_entity_id
+ == "sensor.mock_max_power_sensor"
+ )
- assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
+ assert (
+ entity._presence_manager.presence_sensor_entity_id
+ == "binary_sensor.mock_presence_sensor"
+ )
entity.remove_thermostat()
@@ -334,9 +348,9 @@ async def test_full_over_switch_with_central_config(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 30,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
@@ -387,29 +401,41 @@ async def test_full_over_switch_with_central_config(
assert entity.proportional_algorithm._tpi_coef_int == 0.5
assert entity.proportional_algorithm._tpi_coef_ext == 0.02
assert entity.proportional_algorithm._minimal_activation_delay == 11
- assert entity._security_delay_min == 61
- assert entity._security_min_on_percent == 0.5
- assert entity._security_default_on_percent == 0.2
+ assert entity.safety_manager.safety_delay_min == 61
+ assert entity.safety_manager.safety_min_on_percent == 0.5
+ assert entity.safety_manager.safety_default_on_percent == 0.2
assert entity.is_inversed is False
# We have an entity so window auto is not enabled
- assert entity.is_window_auto_enabled is False
- assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
- assert entity._window_delay_sec == 15
- assert entity._window_auto_close_threshold == 1
- assert entity._window_auto_open_threshold == 4
- assert entity._window_auto_max_duration == 31
+ assert entity.window_manager.is_window_auto_configured is False
+ assert (
+ entity.window_manager._window_sensor_entity_id
+ == "binary_sensor.mock_window_sensor"
+ )
+ assert entity.window_manager.window_delay_sec == 15
+ assert entity.window_manager.window_auto_close_threshold == 1
+ assert entity.window_manager.window_auto_open_threshold == 4
+ assert entity.window_manager.window_auto_max_duration == 31
- assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
- assert entity._motion_delay_sec == 31
- assert entity._motion_off_delay_sec == 301
- assert entity._motion_preset == "boost"
- assert entity._no_motion_preset == "frost"
+ assert (
+ entity.motion_manager.motion_sensor_entity_id
+ == "binary_sensor.mock_motion_sensor"
+ )
+ assert entity.motion_manager.motion_delay_sec == 31
+ assert entity.motion_manager.motion_off_delay_sec == 301
+ assert entity.motion_manager.motion_preset == "boost"
+ assert entity.motion_manager.no_motion_preset == "frost"
- assert entity._power_sensor_entity_id == "sensor.mock_power_sensor"
- assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
+ assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
+ assert (
+ entity.power_manager.max_power_sensor_entity_id
+ == "sensor.mock_max_power_sensor"
+ )
- assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
+ assert (
+ entity._presence_manager.presence_sensor_entity_id
+ == "binary_sensor.mock_presence_sensor"
+ )
entity.remove_thermostat()
@@ -469,7 +495,7 @@ async def test_migration_of_central_config(
central_config_entry = MockConfigEntry(
version=CONFIG_VERSION,
# An old minor version
- minor_version=1,
+ minor_version=0,
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
@@ -483,9 +509,9 @@ async def test_migration_of_central_config(
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
# The old central_boiler parameter
"add_central_boiler_control": True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
diff --git a/tests/test_central_mode.py b/tests/test_central_mode.py
index f598087..742aaef 100644
--- a/tests/test_central_mode.py
+++ b/tests/test_central_mode.py
@@ -56,9 +56,9 @@ async def test_config_with_central_mode_true(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -103,9 +103,9 @@ async def test_config_with_central_mode_false(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -153,9 +153,9 @@ async def test_config_with_central_mode_none(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -205,9 +205,9 @@ async def test_switch_change_central_mode_true(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -347,9 +347,9 @@ async def test_switch_ac_change_central_mode_true(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_AC_MODE: True,
},
)
@@ -482,9 +482,9 @@ async def test_climate_ac_change_central_mode_false(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -624,9 +624,9 @@ async def test_climate_ac_only_change_central_mode_true(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -778,9 +778,9 @@ async def test_switch_change_central_mode_true_with_window(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 0, # To be not obliged to wait
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
@@ -816,7 +816,7 @@ async def test_switch_change_central_mode_true_with_window(
assert entity.hvac_mode == HVACMode.HEAT
assert entity.preset_mode == PRESET_ACTIVITY
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# 2 Open the window
with patch(
@@ -935,9 +935,9 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 0, # To be not obliged to wait
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
@@ -973,7 +973,7 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
assert entity.hvac_mode == HVACMode.HEAT
assert entity.preset_mode == PRESET_ACTIVITY
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# 2 Change central_mode to COOL_ONLY
with patch("homeassistant.core.ServiceRegistry.async_call"):
diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py
index 3b87b63..7e1f4d7 100644
--- a/tests/test_config_flow.py
+++ b/tests/test_config_flow.py
@@ -470,9 +470,9 @@ async def test_user_config_flow_over_climate(
result["flow_id"],
user_input={
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
},
)
assert result["type"] == FlowResultType.MENU
@@ -496,9 +496,9 @@ async def test_user_config_flow_over_climate(
"data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
} | MOCK_DEFAULT_FEATURE_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
@@ -1077,9 +1077,9 @@ async def test_user_config_flow_over_climate_auto_start_stop(
result["flow_id"],
user_input={
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
},
)
assert result["type"] == FlowResultType.MENU
@@ -1104,9 +1104,9 @@ async def test_user_config_flow_over_climate_auto_start_stop(
"data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
} | MOCK_DEFAULT_FEATURE_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
@@ -1274,9 +1274,9 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
result["flow_id"],
user_input={
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
},
)
@@ -1359,9 +1359,9 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.5,
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
@@ -1388,8 +1388,7 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
-# @pytest.mark.parametrize("expected_lingering_timers", [True])
-# @pytest.mark.skip
+@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_climate_valve(
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
@@ -1658,9 +1657,9 @@ async def test_user_config_flow_over_climate_valve(
result["flow_id"],
user_input={
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
},
)
assert result["type"] == FlowResultType.MENU
@@ -1686,9 +1685,9 @@ async def test_user_config_flow_over_climate_valve(
"data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.4,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.4,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
} | MOCK_DEFAULT_FEATURE_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
diff --git a/tests/test_inverted_switch.py b/tests/test_inverted_switch.py
index b0b8834..46f9d72 100644
--- a/tests/test_inverted_switch.py
+++ b/tests/test_inverted_switch.py
@@ -42,8 +42,8 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
diff --git a/tests/test_last_seen.py b/tests/test_last_seen.py
index 906023c..4d35b6d 100644
--- a/tests/test_last_seen.py
+++ b/tests/test_last_seen.py
@@ -25,6 +25,12 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
"""
tz = get_tz(hass) # pylint: disable=invalid-name
+ temps = {
+ "frost": 7,
+ "eco": 17,
+ "comfort": 18,
+ "boost": 19,
+ }
entry = MockConfigEntry(
domain=DOMAIN,
@@ -39,22 +45,18 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
- "frost_temp": 7,
- "eco_temp": 17,
- "comfort_temp": 18,
- "boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
- "heater_entity_id": "switch.mock_switch",
+ CONF_UNDERLYING_LIST: ["switch.mock_switch"],
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
- "security_delay_min": 5, # 5 minutes
- "security_min_on_percent": 0.2,
- "security_default_on_percent": 0.1,
+ CONF_SAFETY_DELAY_MIN: 5, # 5 minutes
+ CONF_SAFETY_MIN_ON_PERCENT: 0.2,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
},
)
@@ -65,8 +67,10 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
)
assert entity
- assert entity._security_state is False
- assert entity.preset_mode is not PRESET_SECURITY
+ await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
+
+ assert entity.safety_manager.is_safety_detected is False
+ assert entity.preset_mode is not PRESET_SAFETY
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
@@ -94,15 +98,15 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
- # set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
+ # set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
- assert entity.security_state is True
- assert entity.preset_mode == PRESET_SECURITY
+ assert entity.safety_state is STATE_ON
+ assert entity.preset_mode == PRESET_SAFETY
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
- call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
+ call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SAFETY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
@@ -135,7 +139,7 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
# 3. change the last seen sensor
event_timestamp = now - timedelta(minutes=4)
await send_last_seen_temperature_change_event(entity, event_timestamp)
- assert entity.security_state is False
+ assert entity.safety_state is not STATE_ON
assert entity.preset_mode is PRESET_COMFORT
assert entity._last_temperature_measure == event_timestamp
diff --git a/tests/test_movement.py b/tests/test_motion.py
similarity index 67%
rename from tests/test_movement.py
rename to tests/test_motion.py
index 6038eea..a89a54d 100644
--- a/tests/test_movement.py
+++ b/tests/test_motion.py
@@ -1,22 +1,288 @@
-# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
+# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable, too-many-lines
""" Test the Window management """
from datetime import datetime, timedelta
import logging
-from unittest.mock import patch
+from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
+from custom_components.versatile_thermostat.feature_motion_manager import (
+ FeatureMotionManager,
+)
+
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
+@pytest.mark.parametrize(
+ "current_state, new_state, temp, nb_call, motion_state, is_motion_detected, preset_refresh, changed",
+ [
+ (STATE_OFF, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
+ # motion is ON. So is_motion_detected is true and preset is BOOST
+ (STATE_OFF, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
+ # current_state is ON and motion is OFF. So is_motion_detected is false and preset is ECO
+ (STATE_ON, STATE_OFF, 17, 1, STATE_OFF, False, PRESET_ECO, True),
+ ],
+)
+async def test_motion_feature_manager_refresh(
+ hass: HomeAssistant,
+ current_state,
+ new_state, # new state of motion event
+ temp,
+ nb_call,
+ motion_state,
+ is_motion_detected,
+ preset_refresh,
+ changed,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_ACTIVITY)
+
+ # 1. creation
+ motion_manager = FeatureMotionManager(fake_vtherm, hass)
+
+ assert motion_manager is not None
+ assert motion_manager.is_configured is False
+ assert motion_manager.is_motion_detected is False
+ assert motion_manager.motion_state == STATE_UNAVAILABLE
+ assert motion_manager.name == "the name"
+
+ assert len(motion_manager._active_listener) == 0
+
+ custom_attributes = {}
+ motion_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["motion_sensor_entity_id"] is None
+ assert custom_attributes["motion_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_motion_configured"] is False
+ assert custom_attributes["motion_preset"] is None
+ assert custom_attributes["no_motion_preset"] is None
+ assert custom_attributes["motion_delay_sec"] == 0
+ assert custom_attributes["motion_off_delay_sec"] == 0
+
+ # 2. post_init
+ motion_manager.post_init(
+ {
+ CONF_MOTION_SENSOR: "sensor.the_motion_sensor",
+ CONF_USE_MOTION_FEATURE: True,
+ CONF_MOTION_DELAY: 10,
+ CONF_MOTION_OFF_DELAY: 30,
+ CONF_MOTION_PRESET: PRESET_BOOST,
+ CONF_NO_MOTION_PRESET: PRESET_ECO,
+ }
+ )
+
+ assert motion_manager.is_configured is True
+ assert motion_manager.motion_state == STATE_UNKNOWN
+ assert motion_manager.is_motion_detected is False
+
+ custom_attributes = {}
+ motion_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
+ assert custom_attributes["motion_state"] == STATE_UNKNOWN
+ assert custom_attributes["is_motion_configured"] is True
+ assert custom_attributes["motion_preset"] is PRESET_BOOST
+ assert custom_attributes["no_motion_preset"] is PRESET_ECO
+ assert custom_attributes["motion_delay_sec"] == 10
+ assert custom_attributes["motion_off_delay_sec"] == 30
+
+ # 3. start listening
+ motion_manager.start_listening()
+ assert motion_manager.is_configured is True
+ assert motion_manager.motion_state == STATE_UNKNOWN
+ assert motion_manager.is_motion_detected is False
+
+ assert len(motion_manager._active_listener) == 1
+
+ # 4. test refresh with the parametrized
+ # fmt:off
+ with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state:
+ # fmt:on
+ # Configurer les méthodes mockées
+ fake_vtherm.find_preset_temp.return_value = temp
+ fake_vtherm.change_target_temperature = AsyncMock()
+ fake_vtherm.async_control_heating = AsyncMock()
+ fake_vtherm.recalculate = MagicMock()
+
+ # force old state for the test
+ motion_manager._motion_state = current_state
+
+ ret = await motion_manager.refresh_state()
+ assert ret == changed
+ assert motion_manager.is_configured is True
+ # in the refresh there is no delay
+ assert motion_manager.motion_state == new_state
+ assert motion_manager.is_motion_detected is is_motion_detected
+
+ assert mock_get_state.call_count == 1
+
+ assert fake_vtherm.find_preset_temp.call_count == nb_call
+
+ if nb_call == 1:
+ fake_vtherm.find_preset_temp.assert_has_calls(
+ [
+ call.find_preset_temp(preset_refresh),
+ ]
+ )
+
+ assert fake_vtherm.change_target_temperature.call_count == nb_call
+ fake_vtherm.change_target_temperature.assert_has_calls(
+ [
+ call.find_preset_temp(temp),
+ ]
+ )
+
+ # We do not call control_heating at startup
+ assert fake_vtherm.recalculate.call_count == 0
+ assert fake_vtherm.async_control_heating.call_count == 0
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ motion_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
+ assert custom_attributes["motion_state"] == new_state
+ assert custom_attributes["is_motion_configured"] is True
+ assert custom_attributes["motion_preset"] is PRESET_BOOST
+ assert custom_attributes["no_motion_preset"] is PRESET_ECO
+ assert custom_attributes["motion_delay_sec"] == 10
+ assert custom_attributes["motion_off_delay_sec"] == 30
+
+ motion_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
+@pytest.mark.parametrize(
+ "current_state, long_enough, new_state, temp, nb_call, motion_state, is_motion_detected, preset_event, changed",
+ [
+ (STATE_OFF, True, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
+ # motion is ON but for not enough time but sensor is on at the end. So is_motion_detected is true and preset is BOOST
+ (STATE_OFF, False, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
+ # motion is OFF for enough time. So is_motion_detected is false and preset is ECO
+ (STATE_ON, True, STATE_OFF, 17, 1, STATE_OFF, False, PRESET_ECO, True),
+ # motion is OFF for not enough time. So is_motion_detected is false and preset is ECO
+ (STATE_ON, False, STATE_OFF, 21, 1, STATE_ON, True, PRESET_BOOST, True),
+ ],
+)
+async def test_motion_feature_manager_event(
+ hass: HomeAssistant,
+ current_state,
+ long_enough,
+ new_state, # new state of motion event
+ temp,
+ nb_call,
+ motion_state,
+ is_motion_detected,
+ preset_event,
+ changed,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_ACTIVITY)
+
+ # 1. iniitialization creation, post_init, start_listening
+ motion_manager = FeatureMotionManager(fake_vtherm, hass)
+ motion_manager.post_init(
+ {
+ CONF_MOTION_SENSOR: "sensor.the_motion_sensor",
+ CONF_USE_MOTION_FEATURE: True,
+ CONF_MOTION_DELAY: 10,
+ CONF_MOTION_OFF_DELAY: 30,
+ CONF_MOTION_PRESET: PRESET_BOOST,
+ CONF_NO_MOTION_PRESET: PRESET_ECO,
+ }
+ )
+ motion_manager.start_listening()
+
+ # 2. test _motion_sensor_changed with the parametrized
+ # fmt: off
+ with patch("homeassistant.helpers.condition.state", return_value=long_enough), \
+ patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)):
+ # fmt: on
+ fake_vtherm.find_preset_temp.return_value = temp
+ fake_vtherm.change_target_temperature = AsyncMock()
+ fake_vtherm.async_control_heating = AsyncMock()
+ fake_vtherm.recalculate = MagicMock()
+
+ # force old state for the test
+ motion_manager._motion_state = current_state
+
+ delay = await motion_manager._motion_sensor_changed(
+ event=Event(
+ event_type=EVENT_STATE_CHANGED,
+ data={
+ "entity_id": "sensor.the_motion_sensor",
+ "new_state": State("sensor.the_motion_sensor", new_state),
+ "old_state": State("sensor.the_motion_sensor", STATE_UNAVAILABLE),
+ }))
+ assert delay is not None
+
+ await delay(None)
+ assert motion_manager.is_configured is True
+ assert motion_manager.motion_state == motion_state
+ assert motion_manager.is_motion_detected is is_motion_detected
+
+ assert fake_vtherm.find_preset_temp.call_count == nb_call
+
+ if nb_call == 1:
+ fake_vtherm.find_preset_temp.assert_has_calls(
+ [
+ call.find_preset_temp(preset_event),
+ ]
+ )
+
+ assert fake_vtherm.change_target_temperature.call_count == nb_call
+ fake_vtherm.change_target_temperature.assert_has_calls(
+ [
+ call.find_preset_temp(temp),
+ ]
+ )
+
+ assert fake_vtherm.recalculate.call_count == 1
+ assert fake_vtherm.async_control_heating.call_count == 1
+ fake_vtherm.async_control_heating.assert_has_calls([
+ call.async_control_heating(force=True)
+ ])
+
+ fake_vtherm.reset_mock()
+
+ # 3. Check custom_attributes
+ custom_attributes = {}
+ motion_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
+ assert custom_attributes["motion_state"] == motion_state
+ assert custom_attributes["is_motion_configured"] is True
+ assert custom_attributes["motion_preset"] is PRESET_BOOST
+ assert custom_attributes["no_motion_preset"] is PRESET_ECO
+ assert custom_attributes["motion_delay_sec"] == 10
+ assert custom_attributes["motion_off_delay_sec"] == 30
+
+ motion_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
-async def test_movement_management_time_not_enough(
+async def test_motion_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
+ temps = {
+ "frost": 10,
+ "eco": 17,
+ "comfort": 18,
+ "boost": 19,
+ "frost_away": 10,
+ "eco_away": 17,
+ "comfort_away": 18,
+ "boost_away": 19,
+ }
entry = MockConfigEntry(
domain=DOMAIN,
@@ -30,23 +296,17 @@ async def test_movement_management_time_not_enough(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
- "eco_temp": 17,
- "comfort_temp": 18,
- "boost_temp": 19,
- "eco_away_temp": 17,
- "comfort_away_temp": 18,
- "boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
- CONF_HEATER: "switch.mock_switch",
+ CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 10,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 10,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10, # important to not been obliged to wait
CONF_MOTION_OFF_DELAY: 30,
@@ -60,11 +320,12 @@ async def test_movement_management_time_not_enough(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
+ await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
- # start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
+ # 1. start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
@@ -75,17 +336,17 @@ async def test_movement_management_time_not_enough(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state is None
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=5)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp)
- assert entity.presence_state == "on"
+ assert entity.presence_state == STATE_ON
- # starts detecting motion with time not enough
+ # 2. starts detecting motion with time not enough
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
@@ -104,7 +365,9 @@ async def test_movement_management_time_not_enough(
),
):
event_timestamp = now - timedelta(minutes=4)
- try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
+ try_condition = await send_motion_change_event(
+ entity, True, False, event_timestamp
+ )
# Will return False -> we will stay on movement False
await try_condition(None)
@@ -137,7 +400,9 @@ async def test_movement_management_time_not_enough(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
event_timestamp = now - timedelta(minutes=3)
- try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
+ try_condition = await send_motion_change_event(
+ entity, True, False, event_timestamp
+ )
# Will return True -> we will switch to movement On
await try_condition(None)
@@ -168,7 +433,9 @@ async def test_movement_management_time_not_enough(
),
):
event_timestamp = now - timedelta(minutes=2)
- try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
+ try_condition = await send_motion_change_event(
+ entity, False, True, event_timestamp
+ )
# Will return False -> we will stay to movement On
await try_condition(None)
@@ -200,7 +467,9 @@ async def test_movement_management_time_not_enough(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
event_timestamp = now - timedelta(minutes=1)
- try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
+ try_condition = await send_motion_change_event(
+ entity, False, True, event_timestamp
+ )
# Will return True -> we will switch to movement Off
await try_condition(None)
@@ -221,7 +490,7 @@ async def test_movement_management_time_not_enough(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
-async def test_movement_management_time_enough_and_presence(
+async def test_motion_management_time_enough_and_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Motion management when time is not enough"""
@@ -253,8 +522,8 @@ async def test_movement_management_time_enough_and_presence(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
@@ -282,8 +551,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state is None
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
@@ -312,9 +581,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 19
- assert entity.motion_state == "on"
- assert entity.presence_state == "on"
-
+ assert entity.motion_state == STATE_ON
+ assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
assert mock_heater_on.call_count == 1
@@ -341,8 +609,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
- assert entity.motion_state == "off"
- assert entity.presence_state == "on"
+ assert entity.motion_state == STATE_OFF
+ assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -353,7 +621,7 @@ async def test_movement_management_time_enough_and_presence(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
-async def test_movement_management_time_enoughand_not_presence(
+async def test_motion_management_time_enough_and_not_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
@@ -385,8 +653,8 @@ async def test_movement_management_time_enoughand_not_presence(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
@@ -414,15 +682,15 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet and presence is unknown
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state is None
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
- assert entity.presence_state == "off"
+ assert entity.presence_state == STATE_OFF
# starts detecting motion
with patch(
@@ -444,8 +712,8 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost away mode
assert entity.target_temperature == 19.1
- assert entity.motion_state == "on"
- assert entity.presence_state == "off"
+ assert entity.motion_state == STATE_ON
+ assert entity.presence_state == STATE_OFF
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
@@ -473,9 +741,8 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18.1
- assert entity.motion_state == "off"
- assert entity.presence_state == "off"
-
+ assert entity.motion_state == STATE_OFF
+ assert entity.presence_state == STATE_OFF
assert mock_send_event.call_count == 0
# 18.1 starts heating with a low on_percent
assert mock_heater_on.call_count == 1
@@ -486,7 +753,7 @@ async def test_movement_management_time_enoughand_not_presence(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
-async def test_movement_management_with_stop_during_condition(
+async def test_motion_management_with_stop_during_condition(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
@@ -518,8 +785,8 @@ async def test_movement_management_with_stop_during_condition(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
@@ -548,15 +815,15 @@ async def test_movement_management_with_stop_during_condition(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state is None
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
- assert entity.presence_state == "off"
+ assert entity.presence_state == STATE_OFF
# starts detecting motion
with patch(
@@ -582,9 +849,8 @@ async def test_movement_management_with_stop_during_condition(
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state == "off"
-
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state == STATE_OFF
# Send a stop detection
event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event(
@@ -595,8 +861,8 @@ async def test_movement_management_with_stop_during_condition(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state == "off"
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state == STATE_OFF
# Resend a start detection
event_timestamp = now - timedelta(minutes=3)
@@ -611,19 +877,19 @@ async def test_movement_management_with_stop_during_condition(
assert entity.preset_mode is PRESET_ACTIVITY
# still no motion detected
assert entity.target_temperature == 18
- assert entity.motion_state is None
- assert entity.presence_state == "off"
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state == STATE_OFF
await try_condition1(None)
# We should have switch this time
assert entity.target_temperature == 19 # Boost
- assert entity.motion_state == "on" # switch to movement on
- assert entity.presence_state == "off" # Non change
+ assert entity.motion_state == STATE_ON # switch to movement on
+ assert entity.presence_state == STATE_OFF # Non change
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
-async def test_movement_management_with_stop_during_condition_last_state_on(
+async def test_motion_management_with_stop_during_condition_last_state_on(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
@@ -655,8 +921,8 @@ async def test_movement_management_with_stop_during_condition_last_state_on(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
@@ -684,7 +950,7 @@ async def test_movement_management_with_stop_during_condition_last_state_on(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
- assert entity.motion_state is None
+ assert entity.motion_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 18, event_timestamp)
diff --git a/tests/test_multiple_switch.py b/tests/test_multiple_switch.py
index 1d78131..e0e8583 100644
--- a/tests/test_multiple_switch.py
+++ b/tests/test_multiple_switch.py
@@ -45,8 +45,8 @@ async def test_one_switch_cycle(
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
@@ -69,7 +69,7 @@ async def test_one_switch_cycle(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNAVAILABLE
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -262,8 +262,8 @@ async def test_multiple_switchs(
CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
@@ -289,7 +289,7 @@ async def test_multiple_switchs(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNAVAILABLE
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -402,8 +402,8 @@ async def test_multiple_climates(
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
@@ -426,7 +426,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNAVAILABLE
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -451,7 +451,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNAVAILABLE
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -503,8 +503,8 @@ async def test_multiple_climates_underlying_changes(
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
@@ -527,7 +527,7 @@ async def test_multiple_climates_underlying_changes(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNAVAILABLE
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -648,8 +648,8 @@ async def test_multiple_climates_underlying_changes_not_aligned(
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
@@ -672,7 +672,7 @@ async def test_multiple_climates_underlying_changes_not_aligned(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNAVAILABLE
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -750,8 +750,8 @@ async def test_multiple_switch_power_management(
CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
@@ -776,17 +776,17 @@ async def test_multiple_switch_power_management(
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
# 1. Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
# 2. Send power max mesurement too low and HVACMode is on
with patch(
@@ -798,10 +798,10 @@ async def test_multiple_switch_power_management(
) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75
await send_max_power_change_event(entity, 74, datetime.now())
- assert await entity.check_overpowering() is True
+ assert await entity.power_manager.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
- assert entity.overpowering_state is True
+ assert entity.power_manager.overpowering_state is STATE_ON
assert entity.target_temperature == 12
assert mock_send_event.call_count == 2
@@ -814,7 +814,7 @@ async def test_multiple_switch_power_management(
"type": "start",
"current_power": 50,
"device_power": 100,
- "current_power_max": 74,
+ "current_max_power": 74,
"current_power_consumption": 25.0,
},
),
@@ -831,7 +831,7 @@ async def test_multiple_switch_power_management(
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
# No change
- assert entity.overpowering_state is True
+ assert entity.power_manager.overpowering_state is STATE_ON
# 4. Send hugh power max mesurement to release overpowering
with patch(
@@ -843,10 +843,10 @@ async def test_multiple_switch_power_management(
) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
await send_max_power_change_event(entity, 150, datetime.now())
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_ECO
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 17
assert (
diff --git a/tests/test_overclimate.py b/tests/test_overclimate.py
index 49b3e9d..1c5efca 100644
--- a/tests/test_overclimate.py
+++ b/tests/test_overclimate.py
@@ -62,8 +62,8 @@ async def test_bug_56(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
@@ -151,7 +151,7 @@ async def test_bug_82(
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False
+ assert entity.safety_manager.is_safety_detected is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
@@ -191,10 +191,10 @@ async def test_bug_82(
):
event_timestamp = now - timedelta(minutes=6)
- # set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
+ # set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False
- assert entity.security_state is False
+ assert entity.safety_state is not STATE_ON
assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none"
@@ -641,8 +641,8 @@ async def test_bug_524(hass: HomeAssistant, skip_hass_states_is_state):
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
@@ -904,8 +904,8 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -938,7 +938,10 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
== AUTO_START_STOP_LEVEL_FAST
)
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
@@ -960,7 +963,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
- assert vtherm.window_state == STATE_OFF
+ assert vtherm.window_state == STATE_UNKNOWN # Cause try_condition is not called
# 2. Open the window and wait for the delay
now = now + timedelta(minutes=2)
@@ -1078,8 +1081,8 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
@@ -1112,7 +1115,10 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
== AUTO_START_STOP_LEVEL_FAST
)
- assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
+ assert (
+ vtherm.auto_start_stop_manager.auto_start_stop_level
+ == AUTO_START_STOP_LEVEL_FAST
+ )
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
@@ -1138,7 +1144,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
now = now + timedelta(minutes=5)
vtherm._set_now(now)
# reset accumulated error (only for testing)
- vtherm._auto_start_stop_algo._accumulated_error = 0
+ vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
diff --git a/tests/test_overclimate_valve.py b/tests/test_overclimate_valve.py
index 8dde208..cbfcbd9 100644
--- a/tests/test_overclimate_valve.py
+++ b/tests/test_overclimate_valve.py
@@ -111,10 +111,10 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
PRESET_BOOST,
]
assert vtherm.preset_mode is PRESET_NONE
- assert vtherm._security_state is False
- assert vtherm._window_state is None
- assert vtherm._motion_state is None
- assert vtherm._presence_state is None
+ assert vtherm.safety_manager.is_safety_detected is False
+ assert vtherm.window_state is STATE_UNAVAILABLE
+ assert vtherm.motion_state is STATE_UNAVAILABLE
+ assert vtherm.presence_state is STATE_UNAVAILABLE
assert vtherm.is_device_active is False
assert vtherm.valve_open_percent == 0
diff --git a/tests/test_power.py b/tests/test_power.py
index 4fdad1c..c2b1828 100644
--- a/tests/test_power.py
+++ b/tests/test_power.py
@@ -1,17 +1,243 @@
# pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Power management """
-from unittest.mock import patch, call
+from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
from datetime import datetime, timedelta
import logging
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
+from custom_components.versatile_thermostat.feature_power_manager import (
+ FeaturePowerManager,
+)
+from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
+@pytest.mark.parametrize(
+ "is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret",
+ [
+ # don't switch to overpower (power is enough)
+ (False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
+ # switch to overpower (power is not enough)
+ (False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
+ # don't switch to overpower (power is not enough but device is already on)
+ (False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
+ # Same with a over_climate
+ # don't switch to overpower (power is enough)
+ (True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
+ # switch to overpower (power is not enough)
+ (True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
+ # don't switch to overpower (power is not enough but device is already on)
+ (True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
+ # Leave overpowering state
+ # switch to not overpower (power is enough)
+ (False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False),
+ # don't switch to overpower (power is still not enough)
+ (False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True),
+ # keep overpower (power is not enough but device is already on)
+ (False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True),
+ ],
+)
+async def test_power_feature_manager(
+ hass: HomeAssistant,
+ is_over_climate,
+ is_device_active,
+ power,
+ max_power,
+ current_overpowering_state,
+ overpowering_state,
+ nb_call,
+ changed,
+ check_overpowering_ret,
+):
+ """Test the FeaturePresenceManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ power_manager = FeaturePowerManager(fake_vtherm, hass)
+
+ assert power_manager is not None
+ assert power_manager.is_configured is False
+ assert power_manager.overpowering_state == STATE_UNAVAILABLE
+ assert power_manager.name == "the name"
+
+ assert len(power_manager._active_listener) == 0
+
+ custom_attributes = {}
+ power_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["power_sensor_entity_id"] is None
+ assert custom_attributes["max_power_sensor_entity_id"] is None
+ assert custom_attributes["overpowering_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_power_configured"] is False
+ assert custom_attributes["device_power"] is 0
+ assert custom_attributes["power_temp"] is None
+ assert custom_attributes["current_power"] is None
+ assert custom_attributes["current_max_power"] is None
+
+ # 2. post_init
+ power_manager.post_init(
+ {
+ CONF_POWER_SENSOR: "sensor.the_power_sensor",
+ CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
+ CONF_USE_POWER_FEATURE: True,
+ CONF_PRESET_POWER: 10,
+ CONF_DEVICE_POWER: 1234,
+ }
+ )
+
+ assert power_manager.is_configured is True
+ assert power_manager.overpowering_state == STATE_UNKNOWN
+
+ custom_attributes = {}
+ power_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
+ assert (
+ custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
+ )
+ assert custom_attributes["overpowering_state"] == STATE_UNKNOWN
+ assert custom_attributes["is_power_configured"] is True
+ assert custom_attributes["device_power"] == 1234
+ assert custom_attributes["power_temp"] == 10
+ assert custom_attributes["current_power"] is None
+ assert custom_attributes["current_max_power"] is None
+
+ # 3. start listening
+ power_manager.start_listening()
+ assert power_manager.is_configured is True
+ assert power_manager.overpowering_state == STATE_UNKNOWN
+
+ assert len(power_manager._active_listener) == 2
+
+ # 4. test refresh and check_overpowering with the parametrized
+ side_effects = SideEffects(
+ {
+ "sensor.the_power_sensor": State("sensor.the_power_sensor", power),
+ "sensor.the_max_power_sensor": State(
+ "sensor.the_max_power_sensor", max_power
+ ),
+ },
+ State("unknown.entity_id", "unknown"),
+ )
+ # fmt:off
+ with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state:
+ # fmt:on
+ # Finish the mock configuration
+ tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm")
+ tpi_algo._on_percent = 1 # pylint: disable="protected-access"
+ type(fake_vtherm).hvac_mode = PropertyMock(return_value=HVACMode.HEAT)
+ type(fake_vtherm).is_device_active = PropertyMock(return_value=is_device_active)
+ type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
+ type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
+ type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
+ type(fake_vtherm)._saved_preset_mode = PropertyMock(return_value=PRESET_ECO)
+
+ fake_vtherm.save_hvac_mode = MagicMock()
+ fake_vtherm.restore_hvac_mode = AsyncMock()
+ fake_vtherm.save_preset_mode = MagicMock()
+ fake_vtherm.restore_preset_mode = AsyncMock()
+ fake_vtherm.async_underlying_entity_turn_off = AsyncMock()
+ fake_vtherm.async_set_preset_mode_internal = AsyncMock()
+ fake_vtherm.send_event = MagicMock()
+ fake_vtherm.update_custom_attributes = MagicMock()
+
+
+ ret = await power_manager.refresh_state()
+ assert ret == changed
+ assert power_manager.is_configured is True
+ assert power_manager.overpowering_state == STATE_UNKNOWN
+ assert power_manager.current_power == power
+ assert power_manager.current_max_power == max_power
+
+ # check overpowering
+ power_manager._overpowering_state = current_overpowering_state
+ ret2 = await power_manager.check_overpowering()
+ assert ret2 == check_overpowering_ret
+ assert power_manager.overpowering_state == overpowering_state
+ assert mock_get_state.call_count == 2
+
+ if power_manager.overpowering_state == STATE_OFF:
+ assert fake_vtherm.save_hvac_mode.call_count == 0
+ assert fake_vtherm.save_preset_mode.call_count == 0
+ assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
+ assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
+ assert fake_vtherm.send_event.call_count == nb_call
+
+ if current_overpowering_state == STATE_ON:
+ assert fake_vtherm.update_custom_attributes.call_count == 1
+ assert fake_vtherm.restore_preset_mode.call_count == 1
+ if is_over_climate:
+ assert fake_vtherm.restore_hvac_mode.call_count == 1
+ else:
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+ else:
+ assert fake_vtherm.update_custom_attributes.call_count == 0
+
+ if nb_call == 1:
+ fake_vtherm.send_event.assert_has_calls(
+ [
+ call.fake_vtherm.send_event(
+ EventType.POWER_EVENT,
+ {'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}),
+ ]
+ )
+
+
+ elif power_manager.overpowering_state == STATE_ON:
+ if is_over_climate:
+ assert fake_vtherm.save_hvac_mode.call_count == 1
+ else:
+ assert fake_vtherm.save_hvac_mode.call_count == 0
+
+ if current_overpowering_state == STATE_OFF:
+ assert fake_vtherm.save_preset_mode.call_count == 1
+ assert fake_vtherm.async_underlying_entity_turn_off.call_count == 1
+ assert fake_vtherm.async_set_preset_mode_internal.call_count == 1
+ assert fake_vtherm.send_event.call_count == 1
+ assert fake_vtherm.update_custom_attributes.call_count == 1
+ else:
+ assert fake_vtherm.save_preset_mode.call_count == 0
+ assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
+ assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
+ assert fake_vtherm.send_event.call_count == 0
+ assert fake_vtherm.update_custom_attributes.call_count == 0
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+ assert fake_vtherm.restore_preset_mode.call_count == 0
+
+ if nb_call == 1:
+ fake_vtherm.send_event.assert_has_calls(
+ [
+ call.fake_vtherm.send_event(
+ EventType.POWER_EVENT,
+ {'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}),
+ ]
+ )
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ power_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
+ assert (
+ custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
+ )
+ assert custom_attributes["overpowering_state"] == overpowering_state
+ assert custom_attributes["is_power_configured"] is True
+ assert custom_attributes["device_power"] == 1234
+ assert custom_attributes["power_temp"] == 10
+ assert custom_attributes["current_power"] == power
+ assert custom_attributes["current_max_power"] == max_power
+
+ power_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_off(
@@ -43,8 +269,8 @@ async def test_power_management_hvac_off(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
@@ -63,23 +289,23 @@ async def test_power_management_hvac_off(
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.hvac_mode == HVACMode.OFF
# Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is not complete
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNKNOWN
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
# Send power max mesurement too low but HVACMode is off
with patch(
@@ -90,10 +316,10 @@ async def test_power_management_hvac_off(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
- assert await entity.check_overpowering() is True
+ assert await entity.power_manager.check_overpowering() is True
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is True
+ assert entity.power_manager.overpowering_state is STATE_ON
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -129,8 +355,8 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
@@ -150,17 +376,17 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
# Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
# Send power max mesurement too low and HVACMode is on
with patch(
@@ -171,10 +397,10 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
- assert await entity.check_overpowering() is True
+ assert await entity.power_manager.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
- assert entity.overpowering_state is True
+ assert entity.power_manager.overpowering_state is STATE_ON
assert entity.target_temperature == 12
assert mock_send_event.call_count == 2
@@ -187,7 +413,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"type": "start",
"current_power": 50,
"device_power": 100,
- "current_power_max": 149,
+ "current_max_power": 149,
"current_power_consumption": 100.0,
},
),
@@ -206,10 +432,10 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_power_change_event(entity, 48, datetime.now())
- assert await entity.check_overpowering() is False
+ assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is False
+ assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 19
assert mock_send_event.call_count == 2
@@ -222,7 +448,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"type": "end",
"current_power": 48,
"device_power": 100,
- "current_power_max": 149,
+ "current_max_power": 149,
},
),
],
@@ -265,8 +491,8 @@ async def test_power_management_energy_over_switch(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
@@ -303,7 +529,7 @@ async def test_power_management_energy_over_switch(
assert entity.current_temperature == 15
assert tpi_algo.on_percent == 1
- assert entity.device_power == 100.0
+ assert entity.power_manager.device_power == 100.0
assert mock_send_event.call_count == 2
assert mock_heater_on.call_count == 1
@@ -324,7 +550,7 @@ async def test_power_management_energy_over_switch(
) as mock_heater_off:
await send_temperature_change_event(entity, 18, datetime.now())
assert tpi_algo.on_percent == 0.3
- assert entity.mean_cycle_power == 30.0
+ assert entity.power_manager.mean_cycle_power == 30.0
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -346,7 +572,7 @@ async def test_power_management_energy_over_switch(
) as mock_heater_off:
await send_temperature_change_event(entity, 20, datetime.now())
assert tpi_algo.on_percent == 0.0
- assert entity.mean_cycle_power == 0.0
+ assert entity.power_manager.mean_cycle_power == 0.0
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -394,8 +620,8 @@ async def test_power_management_energy_over_climate(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
@@ -421,7 +647,7 @@ async def test_power_management_energy_over_climate(
assert entity.current_temperature == 15
# Not initialised yet
- assert entity.mean_cycle_power is None
+ assert entity.power_manager.mean_cycle_power is None
assert entity._underlying_climate_start_hvac_action_date is None
# Send a climate_change event with HVACAction=HEATING
diff --git a/tests/test_presence.py b/tests/test_presence.py
new file mode 100644
index 0000000..c376c0d
--- /dev/null
+++ b/tests/test_presence.py
@@ -0,0 +1,178 @@
+# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
+
+""" Test the Security featrure """
+import logging
+from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
+
+# from datetime import timedelta, datetime
+
+from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
+from custom_components.versatile_thermostat.feature_presence_manager import (
+ FeaturePresenceManager,
+)
+
+from .commons import *
+
+logging.getLogger().setLevel(logging.DEBUG)
+
+
+@pytest.mark.parametrize(
+ "temp, absence, state, nb_call, presence_state, changed",
+ [
+ (19, False, STATE_ON, 1, STATE_ON, True),
+ (17, True, STATE_OFF, 1, STATE_OFF, True),
+ (19, False, STATE_HOME, 1, STATE_ON, True),
+ (17, True, STATE_NOT_HOME, 1, STATE_OFF, True),
+ (17, False, STATE_UNAVAILABLE, 0, STATE_UNKNOWN, False),
+ (17, False, STATE_UNKNOWN, 0, STATE_UNKNOWN, False),
+ (17, False, "wrong state", 0, STATE_UNKNOWN, False),
+ ],
+)
+async def test_presence_feature_manager(
+ hass: HomeAssistant, temp, absence, state, nb_call, presence_state, changed
+):
+ """Test the FeaturePresenceManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
+
+ # 1. creation
+ presence_manager = FeaturePresenceManager(fake_vtherm, hass)
+
+ assert presence_manager is not None
+ assert presence_manager.is_configured is False
+ assert presence_manager.is_absence_detected is False
+ assert presence_manager.presence_state == STATE_UNAVAILABLE
+ assert presence_manager.name == "the name"
+
+ assert len(presence_manager._active_listener) == 0
+
+ custom_attributes = {}
+ presence_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["presence_sensor_entity_id"] is None
+ assert custom_attributes["presence_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_presence_configured"] is False
+
+ # 2. post_init
+ presence_manager.post_init(
+ {
+ CONF_PRESENCE_SENSOR: "sensor.the_presence_sensor",
+ CONF_USE_PRESENCE_FEATURE: True,
+ }
+ )
+
+ assert presence_manager.is_configured is True
+ assert presence_manager.presence_state == STATE_UNKNOWN
+ assert presence_manager.is_absence_detected is False
+
+ custom_attributes = {}
+ presence_manager.add_custom_attributes(custom_attributes)
+ assert (
+ custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
+ )
+ assert custom_attributes["presence_state"] == STATE_UNKNOWN
+ assert custom_attributes["is_presence_configured"] is True
+
+ # 3. start listening
+ presence_manager.start_listening()
+ assert presence_manager.is_configured is True
+ assert presence_manager.presence_state == STATE_UNKNOWN
+ assert presence_manager.is_absence_detected is False
+
+ assert len(presence_manager._active_listener) == 1
+
+ # 4. test refresh with the parametrized
+ # fmt:off
+ with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_presence_sensor", state)) as mock_get_state:
+ # fmt:on
+ # Configurer les méthodes mockées
+ fake_vtherm.find_preset_temp.return_value = temp
+ fake_vtherm.change_target_temperature = AsyncMock()
+ fake_vtherm.async_control_heating = AsyncMock()
+
+ ret = await presence_manager.refresh_state()
+ assert ret == changed
+ assert presence_manager.is_configured is True
+ assert presence_manager.presence_state == presence_state
+ assert presence_manager.is_absence_detected is absence
+
+ assert mock_get_state.call_count == 1
+
+ assert fake_vtherm.find_preset_temp.call_count == nb_call
+
+ if nb_call == 1:
+ fake_vtherm.find_preset_temp.assert_has_calls(
+ [
+ call.find_preset_temp(PRESET_COMFORT),
+ ]
+ )
+
+ assert fake_vtherm.change_target_temperature.call_count == nb_call
+ fake_vtherm.change_target_temperature.assert_has_calls(
+ [
+ call.find_preset_temp(temp),
+ ]
+ )
+
+ assert fake_vtherm.async_control_heating.call_count == 0
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ presence_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
+ assert custom_attributes["presence_state"] == presence_state
+ assert custom_attributes["is_presence_configured"] is True
+
+ # 6. test _presence_sensor_changed with the parametrized
+ fake_vtherm.find_preset_temp.return_value = temp
+ fake_vtherm.change_target_temperature = AsyncMock()
+ fake_vtherm.async_control_heating = AsyncMock()
+
+ await presence_manager._presence_sensor_changed(
+ event=Event(
+ event_type=EVENT_STATE_CHANGED,
+ data={
+ "entity_id": "sensor.the_presence_sensor",
+ "new_state": State("sensor.the_presence_sensor", state),
+ "old_state": State("sensor.the_presence_sensor", STATE_UNAVAILABLE),
+ }))
+ assert ret == changed
+ assert presence_manager.is_configured is True
+ assert presence_manager.presence_state == presence_state
+ assert presence_manager.is_absence_detected is absence
+
+ assert fake_vtherm.find_preset_temp.call_count == nb_call
+
+ if nb_call == 1:
+ fake_vtherm.find_preset_temp.assert_has_calls(
+ [
+ call.find_preset_temp(PRESET_COMFORT),
+ ]
+ )
+
+ assert fake_vtherm.change_target_temperature.call_count == nb_call
+ fake_vtherm.change_target_temperature.assert_has_calls(
+ [
+ call.find_preset_temp(temp),
+ ]
+ )
+
+ assert fake_vtherm.async_control_heating.call_count == 1
+ fake_vtherm.async_control_heating.assert_has_calls([
+ call.async_control_heating(force=True)
+ ])
+
+ fake_vtherm.reset_mock()
+
+ # 7. Check custom_attributes
+ custom_attributes = {}
+ presence_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
+ assert custom_attributes["presence_state"] == presence_state
+ assert custom_attributes["is_presence_configured"] is True
+
+ presence_manager.stop_listening()
+ await hass.async_block_till_done()
diff --git a/tests/test_security.py b/tests/test_safety.py
similarity index 72%
rename from tests/test_security.py
rename to tests/test_safety.py
index 1d37b19..391a137 100644
--- a/tests/test_security.py
+++ b/tests/test_safety.py
@@ -1,7 +1,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the Security featrure """
-from unittest.mock import patch, call
+from unittest.mock import patch, call, PropertyMock, MagicMock
from datetime import timedelta, datetime
import logging
@@ -11,12 +11,103 @@ from custom_components.versatile_thermostat.thermostat_climate import (
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
+from custom_components.versatile_thermostat.feature_safety_manager import (
+ FeatureSafetyManager,
+)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
-
logging.getLogger().setLevel(logging.DEBUG)
+async def test_safety_feature_manager_create(
+ hass: HomeAssistant,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ safety_manager = FeatureSafetyManager(fake_vtherm, hass)
+
+ assert safety_manager is not None
+ assert safety_manager.is_configured is False
+ assert safety_manager.is_safety_detected is False
+ assert safety_manager.safety_state is STATE_UNAVAILABLE
+ assert safety_manager.name == "the name"
+
+ assert len(safety_manager._active_listener) == 0
+
+ custom_attributes = {}
+ safety_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["is_safety_configured"] is False
+ assert custom_attributes["safety_state"] is STATE_UNAVAILABLE
+ assert custom_attributes.get("safety_delay_min", None) is None
+ assert custom_attributes.get("safety_min_on_percent", None) is None
+ assert custom_attributes.get("safety_default_on_percent", None) is None
+
+
+@pytest.mark.parametrize(
+ "safety_delay_min, safety_min_on_percent, safety_default_on_percent, is_configured, state",
+ [
+ # fmt: off
+ ( 10, 11, 12, True, STATE_UNKNOWN),
+ ( None, 11, 12, False, STATE_UNAVAILABLE),
+ ( 10, None, 12, True, STATE_UNKNOWN),
+ ( 10, 11, None, True, STATE_UNKNOWN),
+ ( 10, None, None, True, STATE_UNKNOWN),
+ ( None, None, None, False, STATE_UNAVAILABLE),
+ # fmt: on
+ ],
+)
+async def test_safety_feature_manager_post_init(
+ hass: HomeAssistant,
+ safety_delay_min,
+ safety_min_on_percent,
+ safety_default_on_percent,
+ is_configured,
+ state,
+):
+ """Test the FeatureSafetyManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ safety_manager = FeatureSafetyManager(fake_vtherm, hass)
+ assert safety_manager is not None
+
+ # 2. post_init
+ safety_manager.post_init(
+ {
+ 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,
+ }
+ )
+
+ assert safety_manager.is_configured is is_configured
+ assert safety_manager.safety_state is state
+
+ custom_attributes = {}
+ safety_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["is_safety_configured"] is is_configured
+ assert custom_attributes["safety_state"] is state
+
+ if safety_manager.is_configured:
+ assert custom_attributes.get("safety_delay_min", None) == safety_delay_min
+ assert (
+ custom_attributes.get("safety_min_on_percent", None)
+ == safety_min_on_percent
+ or DEFAULT_SAFETY_MIN_ON_PERCENT
+ )
+ assert (
+ custom_attributes.get("safety_default_on_percent", None)
+ == safety_default_on_percent
+ or DEFAULT_SAFETY_DEFAULT_ON_PERCENT
+ )
+
+
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
@@ -24,17 +115,26 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
1. creates a thermostat and check that security is off
2. activate security feature when date is expired
3. change the preset to boost
- 4. check that security is still on
5. resolve the date issue
6. check that security is off and preset is changed to boost
"""
tz = get_tz(hass) # pylint: disable=invalid-name
+ temps = {
+ "frost": 7,
+ "eco": 17,
+ "comfort": 18,
+ "boost": 19,
+ }
+
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
+ # With migration
+ version=CONFIG_VERSION,
+ minor_version=0,
data={
"name": "TheOverSwitchMockName",
"thermostat_type": "thermostat_over_switch",
@@ -43,15 +143,11 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
- "frost_temp": 7,
- "eco_temp": 17,
- "comfort_temp": 18,
- "boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
- "heater_entity_id": "switch.mock_switch",
+ CONF_UNDERLYING_LIST: ["switch.mock_switch"],
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
@@ -69,8 +165,11 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
)
assert entity
- assert entity._security_state is False
- assert entity.preset_mode is not PRESET_SECURITY
+ await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
+
+ assert entity.safety_manager.safety_state is not STATE_ON
+ assert entity.safety_manager.is_safety_detected is False
+ assert entity.preset_mode is not PRESET_SAFETY
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
@@ -105,8 +204,8 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
- assert entity.security_state is True
- assert entity.preset_mode == PRESET_SECURITY
+ assert entity.safety_state is STATE_ON
+ assert entity.preset_mode == PRESET_SAFETY
assert entity._saved_preset_mode == PRESET_COMFORT
assert entity._prop_algorithm.on_percent == 0.1
assert entity._prop_algorithm.calculated_on_percent == 0.9
@@ -114,7 +213,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
- call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
+ call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SAFETY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
@@ -151,11 +250,13 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_BOOST)
# 4. check that security is still on
- assert entity._security_state is True
+ assert entity.safety_manager.safety_state is STATE_ON
+ assert entity.safety_manager.is_safety_detected is True
+
assert entity._prop_algorithm.on_percent == 0.1
assert entity._prop_algorithm.calculated_on_percent == 0.9
assert entity._saved_preset_mode == PRESET_BOOST
- assert entity.preset_mode is PRESET_SECURITY
+ assert entity.preset_mode is PRESET_SAFETY
# 5. resolve the datetime issue
with patch(
@@ -168,7 +269,9 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15.2, event_timestamp)
- assert entity._security_state is False
+ assert entity.safety_manager.safety_state is not STATE_ON
+ assert entity.safety_manager.is_safety_detected is False
+
assert entity.preset_mode == PRESET_BOOST
assert entity._saved_preset_mode == PRESET_BOOST
assert entity._prop_algorithm.on_percent == 1.0
@@ -215,6 +318,12 @@ async def test_security_feature_back_on_percent(
"""
tz = get_tz(hass) # pylint: disable=invalid-name
+ temps = {
+ "eco": 17,
+ "comfort": 18,
+ "boost": 19,
+ "frost": 10,
+ }
entry = MockConfigEntry(
domain=DOMAIN,
@@ -228,21 +337,18 @@ async def test_security_feature_back_on_percent(
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
- "eco_temp": 17,
- "comfort_temp": 18,
- "boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
- "heater_entity_id": "switch.mock_switch",
+ CONF_UNDERLYING_LIST: ["switch.mock_switch"],
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
- "security_delay_min": 5, # 5 minutes
- "security_min_on_percent": 0.2,
- "security_default_on_percent": 0.1,
+ "safety_delay_min": 5, # 5 minutes
+ "safety_min_on_percent": 0.2,
+ "safety_default_on_percent": 0.1,
},
)
@@ -253,8 +359,12 @@ async def test_security_feature_back_on_percent(
)
assert entity
- assert entity._security_state is False
- assert entity.preset_mode is not PRESET_SECURITY
+ await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
+
+ assert entity.safety_manager.safety_state is not STATE_ON
+ assert entity.safety_manager.is_safety_detected is False
+
+ assert entity.preset_mode is not PRESET_SAFETY
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
@@ -285,7 +395,7 @@ async def test_security_feature_back_on_percent(
await send_temperature_change_event(entity, 17, event_timestamp)
assert entity._prop_algorithm.calculated_on_percent == 0.6
assert entity.preset_mode == PRESET_BOOST
- assert entity.security_state is False
+ assert entity.safety_state is not STATE_ON
assert mock_send_event.call_count == 0
# 3. Set safety mode with a preset change
@@ -302,14 +412,14 @@ async def test_security_feature_back_on_percent(
assert entity._prop_algorithm.calculated_on_percent == 0.6
- assert entity.security_state is True
- assert entity.preset_mode == PRESET_SECURITY
+ assert entity.safety_state is STATE_ON
+ assert entity.preset_mode == PRESET_SAFETY
assert entity._saved_preset_mode == PRESET_BOOST
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
- call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
+ call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SAFETY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
@@ -343,8 +453,8 @@ async def test_security_feature_back_on_percent(
entity._set_now(event_timestamp) # pylint: disable=protected-access
await entity.async_set_preset_mode(PRESET_ECO)
- assert entity.security_state is True
- assert entity.preset_mode == PRESET_SECURITY
+ assert entity.safety_state is STATE_ON
+ assert entity.preset_mode == PRESET_SAFETY
# 5. resolve the datetime issue
with patch(
@@ -359,7 +469,9 @@ async def test_security_feature_back_on_percent(
# set temperature to 18.9 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 18.92, event_timestamp)
- assert entity._security_state is False
+ assert entity.safety_manager.safety_state is not STATE_ON
+ assert entity.safety_manager.is_safety_detected is False
+
assert entity.preset_mode == PRESET_ECO
assert entity._saved_preset_mode == PRESET_ECO
assert entity._prop_algorithm.on_percent == 0.0
@@ -452,7 +564,8 @@ async def test_security_over_climate(
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False
+ assert entity.safety_manager.safety_state is not STATE_ON
+ assert entity.safety_manager.is_safety_detected is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
@@ -506,6 +619,56 @@ async def test_security_over_climate(
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in safety mode
- assert entity.security_state is False
+ assert entity.safety_state is not STATE_ON
assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none"
+
+
+async def test_migration_security_safety(
+ hass: HomeAssistant,
+ skip_hass_states_is_state,
+):
+ """Tests the migration of security parameters to safety in English"""
+ central_config_entry = MockConfigEntry(
+ # Current is 2.1
+ version=CONFIG_VERSION,
+ # An old minor version
+ minor_version=0,
+ domain=DOMAIN,
+ title="TheCentralConfigMockName",
+ unique_id="centralConfigUniqueId",
+ data={
+ CONF_NAME: "migrationName",
+ CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
+ CONF_UNDERLYING_LIST: ["switch.under1"],
+ "security_delay_min": 61,
+ "security_min_on_percent": 0.5,
+ "security_default_on_percent": 0.2,
+ CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
+ CONF_CYCLE_MIN: 5,
+ CONF_DEVICE_POWER: 1,
+ CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
+ CONF_TPI_COEF_INT: 0.3,
+ CONF_TPI_COEF_EXT: 0.1,
+ CONF_USE_MAIN_CENTRAL_CONFIG: False,
+ CONF_USE_WINDOW_FEATURE: False,
+ CONF_USE_MOTION_FEATURE: False,
+ CONF_USE_POWER_FEATURE: False,
+ CONF_USE_PRESENCE_FEATURE: False,
+ CONF_MINIMAL_ACTIVATION_DELAY: 10,
+ },
+ )
+
+ central_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(central_config_entry.entry_id)
+ assert central_config_entry.state is ConfigEntryState.LOADED
+
+ entity: ThermostatOverSwitch = search_entity(
+ hass, "climate.migrationname", "climate"
+ )
+
+ assert entity is not None
+
+ assert entity.safety_manager.safety_min_on_percent == 0.5
+ assert entity.safety_manager.safety_default_on_percent == 0.2
+ assert entity.safety_manager.safety_delay_min == 61
diff --git a/tests/test_sensors.py b/tests/test_sensors.py
index 18dde95..e9a482f 100644
--- a/tests/test_sensors.py
+++ b/tests/test_sensors.py
@@ -62,8 +62,8 @@ async def test_sensors_over_switch(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 200,
},
)
@@ -222,8 +222,8 @@ async def test_sensors_over_climate(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 1.5,
@@ -360,8 +360,8 @@ async def test_sensors_over_climate_minimal(
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
},
)
diff --git a/tests/test_start.py b/tests/test_start.py
index b227bb0..792c339 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -53,10 +53,10 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False
- assert entity._window_state is None
- assert entity._motion_state is None
- assert entity._presence_state is None
+ assert entity.safety_manager.is_safety_detected is False
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None
assert entity.have_valve_regulation is False
@@ -112,10 +112,10 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False
- assert entity._window_state is None
- assert entity._motion_state is None
- assert entity._presence_state is None
+ assert entity.safety_manager.is_safety_detected is False
+ assert entity.window_state is STATE_UNAVAILABLE
+ assert entity.motion_state is STATE_UNAVAILABLE
+ assert entity.presence_state is STATE_UNAVAILABLE
assert entity.have_valve_regulation is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
@@ -151,18 +151,6 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname")
- # entry.add_to_hass(hass)
- # await hass.config_entries.async_setup(entry.entry_id)
- # assert entry.state is ConfigEntryState.LOADED
- #
- # def find_my_entity(entity_id) -> ClimateEntity:
- # """Find my new entity"""
- # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
- # for entity in component.entities:
- # if entity.entity_id == entity_id:
- # return entity
- #
- # entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
assert entity
@@ -180,10 +168,10 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False
- assert entity._window_state is None
- assert entity._motion_state is None
- assert entity._presence_state is None
+ assert entity.safety_manager.is_safety_detected is False
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None
assert entity.nb_underlying_entities == 4
@@ -242,7 +230,7 @@ async def test_over_switch_deactivate_preset(
CONF_HEATER_3: None,
CONF_HEATER_4: None,
CONF_HEATER_KEEP_ALIVE: 0,
- CONF_SECURITY_DELAY_MIN: 10,
+ CONF_SAFETY_DELAY_MIN: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.6,
diff --git a/tests/test_switch_ac.py b/tests/test_switch_ac.py
index 043f9fa..55f17ce 100644
--- a/tests/test_switch_ac.py
+++ b/tests/test_switch_ac.py
@@ -89,10 +89,10 @@ async def test_over_switch_ac_full_start(
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False # pylint: disable=protected-access
- assert entity._window_state is None # pylint: disable=protected-access
- assert entity._motion_state is None # pylint: disable=protected-access
- assert entity._presence_state is None # pylint: disable=protected-access
+ assert entity.safety_manager.is_safety_detected is False
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
@@ -114,7 +114,7 @@ async def test_over_switch_ac_full_start(
event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, True, False, event_timestamp)
- assert entity._presence_state == STATE_ON # pylint: disable=protected-access
+ assert entity.presence_state == STATE_ON # pylint: disable=protected-access
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
@@ -131,7 +131,7 @@ async def test_over_switch_ac_full_start(
# Unset the presence
event_timestamp = now - timedelta(minutes=3)
await send_presence_change_event(entity, False, True, event_timestamp)
- assert entity._presence_state == STATE_OFF # pylint: disable=protected-access
+ assert entity.presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.target_temperature == 27 # eco_ac_away
# Open a window
diff --git a/tests/test_switch_keep_alive.py b/tests/test_switch_keep_alive.py
index c8dc846..3642742 100644
--- a/tests/test_switch_keep_alive.py
+++ b/tests/test_switch_keep_alive.py
@@ -39,8 +39,8 @@ def config_entry() -> MockConfigEntry:
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.1,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.1,
},
)
diff --git a/tests/test_temp_number.py b/tests/test_temp_number.py
index 0100cb6..7449fb1 100644
--- a/tests/test_temp_number.py
+++ b/tests/test_temp_number.py
@@ -75,9 +75,9 @@ async def test_add_number_for_central_config(
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
}
@@ -170,9 +170,9 @@ async def test_add_number_for_central_config_without_temp(
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
},
# | temps,
@@ -265,9 +265,9 @@ async def test_add_number_for_central_config_without_temp_ac_mode(
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
},
# | temps,
@@ -359,9 +359,9 @@ async def test_add_number_for_central_config_without_temp_restore(
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
- CONF_SECURITY_DELAY_MIN: 61,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
+ CONF_SAFETY_DELAY_MIN: 61,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
},
# | temps,
diff --git a/tests/test_tpi.py b/tests/test_tpi.py
index 33f1af9..a033ec5 100644
--- a/tests/test_tpi.py
+++ b/tests/test_tpi.py
@@ -38,8 +38,8 @@ async def test_tpi_calculation(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
# CONF_DEVICE_POWER: 100,
},
)
@@ -58,7 +58,7 @@ async def test_tpi_calculation(
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0
- assert entity.mean_cycle_power is None # no device power configured
+ assert entity.power_manager.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4
@@ -66,14 +66,14 @@ async def test_tpi_calculation(
assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180
- tpi_algo.set_security(0.1)
+ tpi_algo.set_safety(0.1)
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.1
assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
assert tpi_algo.off_time_sec == 270
- tpi_algo.unset_security()
+ tpi_algo.unset_safety()
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4
@@ -87,30 +87,30 @@ async def test_tpi_calculation(
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
- tpi_algo.set_security(0.09)
+ tpi_algo.set_safety(0.09)
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
- tpi_algo.unset_security()
+ tpi_algo.unset_safety()
tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0
- assert entity.mean_cycle_power is None # no device power configured
+ assert entity.power_manager.mean_cycle_power is None # no device power configured
- tpi_algo.set_security(0.09)
+ tpi_algo.set_safety(0.09)
tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
- assert entity.mean_cycle_power is None # no device power configured
+ assert entity.power_manager.mean_cycle_power is None # no device power configured
- tpi_algo.unset_security()
+ tpi_algo.unset_safety()
# The calculated values for HVACMode.OFF are the same as for HVACMode.HEAT.
tpi_algo.calculate(15, 10, 7, HVACMode.OFF)
assert tpi_algo.on_percent == 1
diff --git a/tests/test_valve.py b/tests/test_valve.py
index 70023d4..0042d0b 100644
--- a/tests/test_valve.py
+++ b/tests/test_valve.py
@@ -60,8 +60,8 @@ async def test_over_valve_full_start(
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.3,
CONF_PRESET_POWER: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 100,
CONF_AC_MODE: False,
},
@@ -98,10 +98,12 @@ async def test_over_valve_full_start(
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
- assert entity._security_state is False # pylint: disable=protected-access
- assert entity._window_state is None # pylint: disable=protected-access
- assert entity._motion_state is None # pylint: disable=protected-access
- assert entity._presence_state is None # pylint: disable=protected-access
+ assert (
+ entity.safety_manager.is_safety_detected is False
+ ) # pylint: disable=protected-access
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.motion_state is STATE_UNKNOWN
+ assert entity.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None # pylint: disable=protected-access
assert entity.have_valve_regulation is False
@@ -350,8 +352,8 @@ async def test_over_valve_regulation(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 60,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 60,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
# only send new valve open percent if dtemp is > 30%
CONF_AUTO_REGULATION_DTEMP: 5,
# only send new valve open percent last mesure was more than 5 min ago
@@ -589,7 +591,7 @@ async def test_bug_533(
CONF_VALVE: "number.mock_valve",
CONF_AUTO_REGULATION_DTEMP: 10, # This parameter makes the bug
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 60,
+ CONF_SAFETY_DELAY_MIN: 60,
},
# | temps,
)
diff --git a/tests/test_window.py b/tests/test_window.py
index 05da471..29adb62 100644
--- a/tests/test_window.py
+++ b/tests/test_window.py
@@ -9,11 +9,11 @@ from custom_components.versatile_thermostat.base_thermostat import BaseThermosta
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
+
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
-
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_not_enough(
@@ -45,8 +45,8 @@ async def test_window_management_time_not_enough(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
@@ -65,10 +65,10 @@ async def test_window_management_time_not_enough(
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
with patch(
@@ -134,8 +134,8 @@ async def test_window_management_time_enough(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
@@ -154,10 +154,10 @@ async def test_window_management_time_enough(
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# change temperature to force turning on the heater
with patch(
@@ -281,8 +281,8 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
@@ -304,11 +304,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled is True
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_manager.is_window_auto_configured is True
# Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -337,8 +337,12 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_send_event.call_count == 0
assert entity.is_device_active is True
assert entity.last_temperature_slope == 0.0
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.hvac_mode is HVACMode.HEAT
# send one degre down in one minute
@@ -361,8 +365,10 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count >= 1
assert entity.last_temperature_slope == -6.24
- assert entity._window_auto_algo.is_window_open_detected() is True
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert entity.window_manager._window_auto_algo.is_window_open_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.OFF
@@ -397,8 +403,10 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == -7.49
- assert entity._window_auto_algo.is_window_open_detected() is True
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert entity.window_manager._window_auto_algo.is_window_open_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.OFF
@@ -438,8 +446,12 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == 0.42
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is True
+ )
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
@@ -478,9 +490,10 @@ async def test_window_auto_fast_and_sensor(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
+ CONF_WINDOW_DELAY: 10,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
@@ -504,8 +517,10 @@ async def test_window_auto_fast_and_sensor(
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 21
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled is False
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_auto_state is STATE_UNAVAILABLE
+ assert entity.window_manager.is_window_auto_configured is False
+ assert entity.window_manager.is_configured is True
# Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -534,8 +549,12 @@ async def test_window_auto_fast_and_sensor(
assert mock_send_event.call_count == 0
assert entity.is_device_active is True
assert entity.last_temperature_slope == 0.0
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.hvac_mode is HVACMode.HEAT
# send one degre down in one minute
@@ -559,9 +578,11 @@ async def test_window_auto_fast_and_sensor(
assert entity.last_temperature_slope == -6.24
# The window open should be detected (but not used)
# because we need to calculate the slope anyway, we have the algorithm running
- assert entity._window_auto_algo.is_window_open_detected() is True
- assert entity._window_auto_algo.is_window_close_detected() is False
- assert entity.window_auto_state == STATE_OFF
+ assert entity.window_manager._window_auto_algo.is_window_open_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
+ assert entity.window_auto_state == STATE_UNAVAILABLE
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
@@ -594,8 +615,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
CONF_WINDOW_AUTO_MAX_DURATION: 1, # 0 will deactivate window auto detection
@@ -617,11 +638,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled is True
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_manager.is_window_auto_configured is True
# 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -647,8 +668,12 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
# The climate turns on but was alredy on
assert mock_set_hvac_mode.call_count == 0
assert entity.last_temperature_slope == 0.0
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.hvac_mode is HVACMode.HEAT
# 3. send one degre down in one minute
@@ -665,8 +690,10 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
assert entity.last_temperature_slope == -6.24
- assert entity._window_auto_algo.is_window_open_detected() is True
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert entity.window_manager._window_auto_algo.is_window_open_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert mock_send_event.call_count == 2
# The heater turns off
@@ -714,8 +741,12 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert mock_set_hvac_mode.call_count == 1
assert round(entity.last_temperature_slope, 3) == -0.29
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
# Clean the entity
entity.remove_thermostat()
@@ -752,11 +783,11 @@ async def test_window_auto_no_on_percent(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
- CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
+ CONF_WINDOW_AUTO_MAX_DURATION: 1, # Should be 0 for test but 0 is not possible
},
)
@@ -775,10 +806,11 @@ async def test_window_auto_no_on_percent(
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 20
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_auto_state is STATE_UNKNOWN
# Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -806,8 +838,12 @@ async def test_window_auto_no_on_percent(
# The heater don't turns on
assert mock_heater_on.call_count == 0
assert entity.last_temperature_slope == 0.0
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.hvac_mode is HVACMode.HEAT
assert entity.proportional_algorithm.on_percent == 0.0
@@ -833,10 +869,12 @@ async def test_window_auto_no_on_percent(
assert mock_heater_off.call_count == 1
assert entity.last_temperature_slope == -6.24
# The algo calculate open ...
- assert entity._window_auto_algo.is_window_open_detected() is True
- assert entity._window_auto_algo.is_window_close_detected() is False
- # But the entity is still on
- assert entity.window_auto_state == STATE_OFF
+ assert entity.window_manager._window_auto_algo.is_window_open_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
+ # But the entity is still on and window_auto is not detected
+ assert entity.window_auto_state == STATE_UNKNOWN
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
@@ -872,8 +910,8 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
@@ -891,11 +929,11 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled is False
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_manager.is_window_auto_configured is False
# change temperature to force turning on the heater
with patch(
@@ -918,9 +956,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
# Set Window ByPass to true
await entity.service_set_window_bypass_state(True)
- assert entity.window_bypass_state is True
-
- # entity._window_bypass_state = True
+ assert entity.is_window_bypass is True
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
@@ -936,7 +972,10 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
new_callable=PropertyMock,
return_value=True,
):
- await send_window_change_event(entity, True, False, datetime.now())
+ try_function = await send_window_change_event(
+ entity, True, False, datetime.now()
+ )
+ await try_function(None)
assert mock_send_event.call_count == 0
@@ -944,7 +983,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_heater_on.call_count == 0
# One call in set_hvac_mode turn_off and one call in the control_heating for security
assert mock_heater_off.call_count == 0
- assert mock_condition.call_count == 1
+ assert mock_condition.call_count > 0
assert entity.hvac_mode is HVACMode.HEAT
assert entity.window_state == STATE_ON
@@ -1011,8 +1050,8 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
CONF_WINDOW_AUTO_MAX_DURATION: 1, # Should be > 0 to activate window_auto
@@ -1034,11 +1073,11 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_manager.is_window_auto_configured
# Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -1066,13 +1105,17 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
# The heater turns on
assert entity.is_device_active is True
assert entity.last_temperature_slope == 0.0
- assert entity._window_auto_algo.is_window_open_detected() is False
- assert entity._window_auto_algo.is_window_close_detected() is False
+ assert (
+ entity.window_manager._window_auto_algo.is_window_open_detected() is False
+ )
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
assert entity.hvac_mode is HVACMode.HEAT
# send one degre down in one minute with window bypass on
await entity.service_set_window_bypass_state(True)
- assert entity.window_bypass_state is True
+ assert entity.is_window_bypass is True
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
@@ -1094,9 +1137,11 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == -6.24
- assert entity._window_auto_algo.is_window_open_detected() is True
- assert entity._window_auto_algo.is_window_close_detected() is False
- assert entity.window_auto_state == STATE_OFF
+ assert entity.window_manager._window_auto_algo.is_window_open_detected() is True
+ assert (
+ entity.window_manager._window_auto_algo.is_window_close_detected() is False
+ )
+ assert entity.window_auto_state == STATE_UNKNOWN
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
@@ -1133,8 +1178,8 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
@@ -1152,10 +1197,10 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# change temperature to force turning on the heater
with patch(
@@ -1262,8 +1307,8 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 1,
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
@@ -1299,7 +1344,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
assert entity
assert entity.is_over_climate is True
- assert entity.window_action == CONF_WINDOW_FAN_ONLY
+ assert entity.window_manager.window_action == CONF_WINDOW_FAN_ONLY
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
@@ -1307,7 +1352,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 18
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# 2. Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
@@ -1419,8 +1464,8 @@ async def test_window_action_fan_only_ko(
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 1,
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
@@ -1456,7 +1501,7 @@ async def test_window_action_fan_only_ko(
assert entity
assert entity.is_over_climate is True
- assert entity.window_action == CONF_WINDOW_FAN_ONLY
+ assert entity.window_manager.window_action == CONF_WINDOW_FAN_ONLY
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
@@ -1464,7 +1509,7 @@ async def test_window_action_fan_only_ko(
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 18
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# 2. Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
@@ -1570,8 +1615,8 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
@@ -1591,11 +1636,11 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled is True
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_manager.is_window_auto_configured is True
# 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -1624,8 +1669,8 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
assert mock_send_event.call_count == 0
assert entity.is_device_active is True
assert entity.hvac_mode is HVACMode.HEAT
- assert entity.window_state is STATE_OFF
- assert entity.window_auto_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_auto_state is STATE_UNKNOWN
# 3. send one degre down in one minute
with patch(
@@ -1648,7 +1693,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == -6.24
assert entity.window_auto_state == STATE_ON
- assert entity.window_state == STATE_OFF
+ assert entity.window_state == STATE_ON
# No change on HVACMode
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
@@ -1767,8 +1812,8 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
@@ -1788,11 +1833,11 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
- assert entity.overpowering_state is None
+ assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21
- assert entity.window_state is STATE_OFF
- assert entity.is_window_auto_enabled is True
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_manager.is_window_auto_configured is True
# 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
@@ -1821,8 +1866,8 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert mock_send_event.call_count == 0
assert entity.is_device_active is True
assert entity.hvac_mode is HVACMode.HEAT
- assert entity.window_state is STATE_OFF
- assert entity.window_auto_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
+ assert entity.window_auto_state is STATE_UNKNOWN
# 3. send one degre down in one minute
with patch(
@@ -1845,7 +1890,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == -6.24
assert entity.window_auto_state == STATE_ON
- assert entity.window_state == STATE_OFF
+ assert entity.window_state == STATE_ON
# No change on HVACMode
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
@@ -1971,9 +2016,9 @@ async def test_bug_66(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.5,
- CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.5,
+ CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1, # !! here
CONF_DEVICE_POWER: 200,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
@@ -1991,7 +2036,7 @@ async def test_bug_66(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
- assert entity.window_state is STATE_OFF
+ assert entity.window_state is STATE_UNKNOWN
# Open the window and let the thermostat shut down
with patch(
@@ -2122,8 +2167,8 @@ async def test_window_action_frost_temp_preset_change(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
CONF_WINDOW_DELAY: 1,
@@ -2150,8 +2195,8 @@ async def test_window_action_frost_temp_preset_change(
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.target_temperature == 21
- assert vtherm.window_state is STATE_OFF
- assert vtherm.is_window_auto_enabled is False
+ assert vtherm.window_state is STATE_UNKNOWN
+ assert vtherm.window_manager.is_window_auto_configured is False
# 1. Turn on the window sensor
now = now + timedelta(minutes=1)
@@ -2232,8 +2277,8 @@ async def test_window_action_frost_temp_temp_change(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
- CONF_SECURITY_DELAY_MIN: 5,
- CONF_SECURITY_MIN_ON_PERCENT: 0.3,
+ CONF_SAFETY_DELAY_MIN: 5,
+ CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
CONF_WINDOW_DELAY: 1,
@@ -2260,8 +2305,8 @@ async def test_window_action_frost_temp_temp_change(
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.target_temperature == 21
- assert vtherm.window_state is STATE_OFF
- assert vtherm.is_window_auto_enabled is False
+ assert vtherm.window_state is STATE_UNKNOWN
+ assert vtherm.window_manager.is_window_auto_configured is False
# 1. Turn on the window sensor
now = now + timedelta(minutes=1)
diff --git a/tests/test_window_feature_manager.py b/tests/test_window_feature_manager.py
new file mode 100644
index 0000000..e6f26ff
--- /dev/null
+++ b/tests/test_window_feature_manager.py
@@ -0,0 +1,706 @@
+# pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines
+""" Test the Window management """
+import logging
+from datetime import datetime, timedelta
+from unittest.mock import patch, call, PropertyMock, AsyncMock, MagicMock
+
+from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
+
+from custom_components.versatile_thermostat.feature_window_manager import (
+ FeatureWindowManager,
+)
+from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
+
+logging.getLogger().setLevel(logging.DEBUG)
+
+
+async def test_window_feature_manager_create(
+ hass: HomeAssistant,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+
+ assert window_manager is not None
+ assert window_manager.is_configured is False
+ assert window_manager.is_window_auto_configured is False
+ assert window_manager.is_window_detected is False
+ assert window_manager.window_state == STATE_UNAVAILABLE
+ assert window_manager.name == "the name"
+
+ assert len(window_manager._active_listener) == 0
+
+ custom_attributes = {}
+ window_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["window_sensor_entity_id"] is None
+ assert custom_attributes["window_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_window_configured"] is False
+ assert custom_attributes["is_window_auto_configured"] is False
+ assert custom_attributes["window_delay_sec"] == 0
+ assert custom_attributes["window_auto_open_threshold"] == 0
+ assert custom_attributes["window_auto_close_threshold"] == 0
+ assert custom_attributes["window_auto_max_duration"] == 0
+ assert custom_attributes["window_action"] is None
+
+
+@pytest.mark.parametrize(
+ "use_window_feature, window_sensor_entity_id, window_delay_sec, window_auto_open_threshold, window_auto_close_threshold, window_auto_max_duration, window_action, is_configured, is_auto_configured, window_state, window_auto_state",
+ [
+ # fmt: off
+ ( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_TURN_OFF, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ),
+ ( False, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_FAN_ONLY, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
+ ( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_FROST_TEMP, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ),
+ # delay is missing
+ ( True, "sensor.the_window_sensor", None, None, None, None, CONF_WINDOW_ECO_TEMP, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
+ # action is missing -> defaults to TURN_OFF
+ ( True, "sensor.the_window_sensor", 10, None, None, None, None, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ),
+ # With Window auto config complete
+ ( True, None, None, 1, 2, 3, CONF_WINDOW_FAN_ONLY, True, True, STATE_UNKNOWN, STATE_UNKNOWN ),
+ # With Window auto config not complete -> missing open threshold but defaults to 0
+ ( True, None, None, None, 2, 3, CONF_WINDOW_FROST_TEMP, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
+ # With Window auto config not complete -> missing close threshold
+ ( True, None, None, 1, None, 3, CONF_WINDOW_ECO_TEMP, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
+ # With Window auto config not complete -> missing max duration threshold but defaults to 0
+ ( True, None, None, 1, 2, None, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
+ # fmt: on
+ ],
+)
+async def test_window_feature_manager_post_init(
+ hass: HomeAssistant,
+ use_window_feature,
+ window_sensor_entity_id,
+ window_delay_sec,
+ window_auto_open_threshold,
+ window_auto_close_threshold,
+ window_auto_max_duration,
+ window_action,
+ is_configured,
+ is_auto_configured,
+ window_state,
+ window_auto_state,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+
+ # 1. creation
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+ assert window_manager is not None
+
+ # 2. post_init
+ window_manager.post_init(
+ {
+ CONF_USE_WINDOW_FEATURE: use_window_feature,
+ CONF_WINDOW_SENSOR: window_sensor_entity_id,
+ CONF_WINDOW_DELAY: window_delay_sec,
+ CONF_WINDOW_AUTO_OPEN_THRESHOLD: window_auto_open_threshold,
+ CONF_WINDOW_AUTO_CLOSE_THRESHOLD: window_auto_close_threshold,
+ CONF_WINDOW_AUTO_MAX_DURATION: window_auto_max_duration,
+ CONF_WINDOW_ACTION: window_action,
+ }
+ )
+
+ assert window_manager.is_configured is is_configured
+ assert window_manager.is_window_auto_configured == is_auto_configured
+ assert window_manager.window_sensor_entity_id == window_sensor_entity_id
+ assert window_manager.window_state == window_state
+ assert window_manager.window_auto_state == window_auto_state
+ assert window_manager.window_delay_sec == window_delay_sec
+ assert window_manager.window_auto_open_threshold == window_auto_open_threshold
+ assert window_manager.window_auto_close_threshold == window_auto_close_threshold
+
+ custom_attributes = {}
+ window_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["window_sensor_entity_id"] == window_sensor_entity_id
+ assert custom_attributes["window_state"] == window_state
+ assert custom_attributes["window_auto_state"] == window_auto_state
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["is_window_configured"] is is_configured
+ assert custom_attributes["is_window_auto_configured"] is is_auto_configured
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["window_delay_sec"] is window_delay_sec
+ assert custom_attributes["window_auto_open_threshold"] is window_auto_open_threshold
+ assert (
+ custom_attributes["window_auto_close_threshold"] is window_auto_close_threshold
+ )
+ assert custom_attributes["window_auto_max_duration"] is window_auto_max_duration
+
+
+@pytest.mark.parametrize(
+ "current_state, new_state, nb_call, window_state, is_window_detected, changed",
+ [
+ (STATE_OFF, STATE_ON, 1, STATE_ON, True, True),
+ (STATE_OFF, STATE_OFF, 0, STATE_OFF, False, False),
+ (STATE_ON, STATE_OFF, 1, STATE_OFF, False, True),
+ (STATE_ON, STATE_ON, 0, STATE_ON, True, False),
+ ],
+)
+async def test_window_feature_manager_refresh_sensor_action_turn_off(
+ hass: HomeAssistant,
+ current_state,
+ new_state, # new state of motion event
+ nb_call,
+ window_state,
+ is_window_detected,
+ changed,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
+
+ # 1. creation
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+
+ # 2. post_init
+ window_manager.post_init(
+ {
+ CONF_WINDOW_SENSOR: "sensor.the_window_sensor",
+ CONF_USE_WINDOW_FEATURE: True,
+ CONF_WINDOW_DELAY: 10,
+ CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
+ }
+ )
+
+ # 3. start listening
+ window_manager.start_listening()
+ assert window_manager.is_configured is True
+ assert window_manager.window_state == STATE_UNKNOWN
+ assert window_manager.window_auto_state == STATE_UNAVAILABLE
+
+ assert len(window_manager._active_listener) == 1
+
+ # 4. test refresh with the parametrized
+ # fmt:off
+ with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state:
+ # fmt:on
+ # Configurer les méthodes mockées
+ fake_vtherm.async_set_hvac_mode = AsyncMock()
+ fake_vtherm.set_hvac_off_reason = MagicMock()
+ fake_vtherm.restore_hvac_mode = AsyncMock()
+
+ # force old state for the test
+ window_manager._window_state = current_state
+ if current_state == STATE_ON:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION)
+ else:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None)
+
+ ret = await window_manager.refresh_state()
+ assert ret == changed
+ assert window_manager.is_configured is True
+ # in the refresh there is no delay
+ assert window_manager.window_state == new_state
+ assert mock_get_state.call_count == 1
+
+ assert fake_vtherm.set_hvac_off_reason.call_count == nb_call
+
+ if nb_call == 1:
+ if new_state == STATE_OFF:
+ assert fake_vtherm.restore_hvac_mode.call_count == 1
+ assert fake_vtherm.async_set_hvac_mode.call_count == 0
+ else:
+ assert fake_vtherm.async_set_hvac_mode.call_count == 1
+ fake_vtherm.async_set_hvac_mode.assert_has_calls(
+ [
+ call.async_set_hvac_mode(HVACMode.OFF),
+ ]
+ )
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+
+ reason = None if current_state == STATE_ON and new_state == STATE_OFF else HVAC_OFF_REASON_WINDOW_DETECTION
+ fake_vtherm.set_hvac_off_reason.assert_has_calls(
+ [
+ call.set_hvac_off_reason(reason),
+ ]
+ )
+
+ else:
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+ assert fake_vtherm.async_set_hvac_mode.call_count == 0
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ window_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor"
+ assert custom_attributes["window_state"] == new_state
+ assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["is_window_configured"] is True
+ assert custom_attributes["is_window_auto_configured"] is False
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["window_delay_sec"] is 10
+ assert custom_attributes["window_auto_open_threshold"] is None
+ assert (
+ custom_attributes["window_auto_close_threshold"] is None
+ )
+ assert custom_attributes["window_auto_max_duration"] is None
+
+ window_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
+@pytest.mark.parametrize(
+ "current_state, new_state, nb_call, window_state, is_window_detected, changed",
+ [
+ (STATE_OFF, STATE_ON, 1, STATE_ON, True, True),
+ (STATE_OFF, STATE_OFF, 0, STATE_OFF, False, False),
+ (STATE_ON, STATE_OFF, 1, STATE_OFF, False, True),
+ (STATE_ON, STATE_ON, 0, STATE_ON, True, False),
+ ],
+)
+async def test_window_feature_manager_refresh_sensor_action_frost_only(
+ hass: HomeAssistant,
+ current_state,
+ new_state, # new state of motion event
+ nb_call,
+ window_state,
+ is_window_detected,
+ changed,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
+ type(fake_vtherm).last_central_mode = PropertyMock(return_value=None)
+
+ # 1. creation
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+
+ # 2. post_init
+ window_manager.post_init(
+ {
+ CONF_WINDOW_SENSOR: "sensor.the_window_sensor",
+ CONF_USE_WINDOW_FEATURE: True,
+ CONF_WINDOW_DELAY: 10,
+ CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
+ }
+ )
+
+ # 3. start listening
+ window_manager.start_listening()
+ assert window_manager.is_configured is True
+ assert window_manager.window_state == STATE_UNKNOWN
+ assert window_manager.window_auto_state == STATE_UNAVAILABLE
+
+ assert len(window_manager._active_listener) == 1
+
+ # 4. test refresh with the parametrized
+ # fmt:off
+ with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state:
+ # fmt:on
+ # Configurer les méthodes mockées
+ fake_vtherm.save_target_temp = AsyncMock()
+ fake_vtherm.set_hvac_off_reason = MagicMock()
+ fake_vtherm.restore_target_temp = AsyncMock()
+ fake_vtherm.change_target_temperature = AsyncMock()
+ fake_vtherm.find_preset_temp = MagicMock()
+ fake_vtherm.find_preset_temp.return_value = 17
+
+ # force old state for the test
+ window_manager._window_state = current_state
+ if current_state == STATE_ON:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION)
+ else:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None)
+
+ ret = await window_manager.refresh_state()
+ assert ret == changed
+ assert window_manager.is_configured is True
+ # in the refresh there is no delay
+ assert window_manager.window_state == new_state
+ assert mock_get_state.call_count == 1
+
+ assert fake_vtherm.set_hvac_off_reason.call_count == 0
+
+ if nb_call == 1:
+ if new_state == STATE_OFF:
+ assert fake_vtherm.restore_target_temp.call_count == 1
+ assert fake_vtherm.save_target_temp.call_count == 0
+ assert fake_vtherm.change_target_temperature.call_count == 0
+ assert fake_vtherm.find_preset_temp.call_count == 0
+ else:
+ assert fake_vtherm.restore_target_temp.call_count == 0
+ assert fake_vtherm.save_target_temp.call_count == 1
+ assert fake_vtherm.change_target_temperature.call_count == 1
+ fake_vtherm.change_target_temperature.assert_has_calls(
+ [
+ call.change_target_temperature(17),
+ ]
+ )
+ assert fake_vtherm.find_preset_temp.call_count == 1
+ else:
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+ assert fake_vtherm.save_target_temp.call_count == 0
+ assert fake_vtherm.change_target_temperature.call_count == 0
+ assert fake_vtherm.find_preset_temp.call_count == 0
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ window_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor"
+ assert custom_attributes["window_state"] == new_state
+ assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["is_window_configured"] is True
+ assert custom_attributes["is_window_auto_configured"] is False
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["window_delay_sec"] is 10
+ assert custom_attributes["window_auto_open_threshold"] is None
+ assert (
+ custom_attributes["window_auto_close_threshold"] is None
+ )
+ assert custom_attributes["window_auto_max_duration"] is None
+
+ window_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
+@pytest.mark.parametrize(
+ "current_state, long_enough, new_state, nb_call, window_state, is_window_detected",
+ [
+ (STATE_OFF, True, STATE_ON, 1, STATE_ON, True),
+ (STATE_OFF, True, STATE_OFF, 0, STATE_OFF, False),
+ (STATE_ON, True, STATE_OFF, 1, STATE_OFF, False),
+ (STATE_ON, True, STATE_ON, 0, STATE_ON, True),
+ (STATE_OFF, False, STATE_ON, 0, STATE_OFF, False),
+ (STATE_ON, False, STATE_OFF, 0, STATE_ON, True),
+ ],
+)
+async def test_window_feature_manager_sensor_event_action_turn_off(
+ hass: HomeAssistant,
+ current_state,
+ long_enough,
+ new_state, # new state of motion event
+ nb_call,
+ window_state,
+ is_window_detected,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
+
+ # 1. creation
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+
+ # 2. post_init
+ window_manager.post_init(
+ {
+ CONF_WINDOW_SENSOR: "sensor.the_window_sensor",
+ CONF_USE_WINDOW_FEATURE: True,
+ CONF_WINDOW_DELAY: 10,
+ CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
+ }
+ )
+
+ # 3. start listening
+ window_manager.start_listening()
+ assert len(window_manager._active_listener) == 1
+
+ # 4. test refresh with the parametrized
+ # fmt:off
+ with patch("homeassistant.helpers.condition.state", return_value=long_enough):
+ # fmt:on
+ # Configurer les méthodes mockées
+ fake_vtherm.async_set_hvac_mode = AsyncMock()
+ fake_vtherm.set_hvac_off_reason = MagicMock()
+ fake_vtherm.restore_hvac_mode = AsyncMock()
+
+ # force old state for the test
+ window_manager._window_state = current_state
+ if current_state == STATE_ON:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION)
+ else:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None)
+
+ try_window_condition = await window_manager._window_sensor_changed(
+ event=Event(
+ event_type=EVENT_STATE_CHANGED,
+ data={
+ "entity_id": "sensor.the_window_sensor",
+ "new_state": State("sensor.the_window_sensor", new_state),
+ "old_state": State("sensor.the_window_sensor", current_state),
+ }))
+ assert try_window_condition is not None
+
+ await try_window_condition(None)
+
+ # There is change only if long enough
+ if long_enough:
+ assert window_manager.window_state == new_state
+ else:
+ assert window_manager.window_state == current_state
+
+ assert fake_vtherm.set_hvac_off_reason.call_count == nb_call
+
+ if nb_call == 1:
+ if new_state == STATE_OFF:
+ assert fake_vtherm.restore_hvac_mode.call_count == 1
+ assert fake_vtherm.async_set_hvac_mode.call_count == 0
+ else:
+ assert fake_vtherm.async_set_hvac_mode.call_count == 1
+ fake_vtherm.async_set_hvac_mode.assert_has_calls(
+ [
+ call.async_set_hvac_mode(HVACMode.OFF),
+ ]
+ )
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+
+ reason = None if current_state == STATE_ON and new_state == STATE_OFF else HVAC_OFF_REASON_WINDOW_DETECTION
+ fake_vtherm.set_hvac_off_reason.assert_has_calls(
+ [
+ call.set_hvac_off_reason(reason),
+ ]
+ )
+
+ else:
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+ assert fake_vtherm.async_set_hvac_mode.call_count == 0
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ window_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor"
+ assert custom_attributes["window_state"] == new_state if long_enough else current_state
+ assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["is_window_configured"] is True
+ assert custom_attributes["is_window_auto_configured"] is False
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["window_delay_sec"] is 10
+ assert custom_attributes["window_auto_open_threshold"] is None
+ assert (
+ custom_attributes["window_auto_close_threshold"] is None
+ )
+ assert custom_attributes["window_auto_max_duration"] is None
+
+ window_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
+@pytest.mark.parametrize(
+ "current_state, long_enough, new_state, nb_call, window_state, is_window_detected",
+ [
+ (STATE_OFF, True, STATE_ON, 1, STATE_ON, True),
+ (STATE_OFF, True, STATE_OFF, 0, STATE_OFF, False),
+ (STATE_ON, True, STATE_OFF, 1, STATE_OFF, False),
+ (STATE_ON, True, STATE_ON, 0, STATE_ON, True),
+ (STATE_OFF, False, STATE_ON, 0, STATE_OFF, False),
+ (STATE_ON, False, STATE_OFF, 0, STATE_ON, True),
+ ],
+)
+async def test_window_feature_manager_event_sensor_action_frost_only(
+ hass: HomeAssistant,
+ current_state,
+ long_enough,
+ new_state, # new state of motion event
+ nb_call,
+ window_state,
+ is_window_detected,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
+ type(fake_vtherm).last_central_mode = PropertyMock(return_value=None)
+
+ # 1. creation
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+
+ # 2. post_init
+ window_manager.post_init(
+ {
+ CONF_WINDOW_SENSOR: "sensor.the_window_sensor",
+ CONF_USE_WINDOW_FEATURE: True,
+ CONF_WINDOW_DELAY: 10,
+ CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
+ }
+ )
+
+ # 3. start listening
+ window_manager.start_listening()
+
+ # 4. test refresh with the parametrized
+ # fmt:off
+ with patch("homeassistant.helpers.condition.state", return_value=long_enough):
+ # fmt:on
+ # Configurer les méthodes mockées
+ fake_vtherm.save_target_temp = AsyncMock()
+ fake_vtherm.set_hvac_off_reason = MagicMock()
+ fake_vtherm.restore_target_temp = AsyncMock()
+ fake_vtherm.change_target_temperature = AsyncMock()
+ fake_vtherm.find_preset_temp = MagicMock()
+ fake_vtherm.find_preset_temp.return_value = 17
+
+ # force old state for the test
+ window_manager._window_state = current_state
+ if current_state == STATE_ON:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION)
+ else:
+ type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None)
+
+ try_window_condition = await window_manager._window_sensor_changed(
+ event=Event(
+ event_type=EVENT_STATE_CHANGED,
+ data={
+ "entity_id": "sensor.the_window_sensor",
+ "new_state": State("sensor.the_window_sensor", new_state),
+ "old_state": State("sensor.the_window_sensor", current_state),
+ }))
+ assert try_window_condition is not None
+
+ await try_window_condition(None)
+
+ if long_enough:
+ assert window_manager.window_state == new_state
+ else:
+ assert window_manager.window_state == current_state
+
+ assert fake_vtherm.set_hvac_off_reason.call_count == 0
+
+ if nb_call == 1:
+ if new_state == STATE_OFF:
+ assert fake_vtherm.restore_target_temp.call_count == 1
+ assert fake_vtherm.save_target_temp.call_count == 0
+ assert fake_vtherm.change_target_temperature.call_count == 0
+ assert fake_vtherm.find_preset_temp.call_count == 0
+ else:
+ assert fake_vtherm.restore_target_temp.call_count == 0
+ assert fake_vtherm.save_target_temp.call_count == 1
+ assert fake_vtherm.change_target_temperature.call_count == 1
+ fake_vtherm.change_target_temperature.assert_has_calls(
+ [
+ call.change_target_temperature(17),
+ ]
+ )
+ assert fake_vtherm.find_preset_temp.call_count == 1
+ else:
+ assert fake_vtherm.restore_hvac_mode.call_count == 0
+ assert fake_vtherm.save_target_temp.call_count == 0
+ assert fake_vtherm.change_target_temperature.call_count == 0
+ assert fake_vtherm.find_preset_temp.call_count == 0
+
+ fake_vtherm.reset_mock()
+
+ # 5. Check custom_attributes
+ custom_attributes = {}
+ window_manager.add_custom_attributes(custom_attributes)
+ assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor"
+ assert custom_attributes["window_state"] == new_state if long_enough else current_state
+ assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["is_window_configured"] is True
+ assert custom_attributes["is_window_auto_configured"] is False
+ assert custom_attributes["is_window_bypass"] is False
+ assert custom_attributes["window_delay_sec"] is 10
+ assert custom_attributes["window_auto_open_threshold"] is None
+ assert (
+ custom_attributes["window_auto_close_threshold"] is None
+ )
+ assert custom_attributes["window_auto_max_duration"] is None
+
+ window_manager.stop_listening()
+ await hass.async_block_till_done()
+
+
+@pytest.mark.parametrize(
+ "current_state, in_cycle, new_temp, new_state, nb_call, window_state, is_window_detected",
+ [
+ (STATE_OFF, True, 10, STATE_ON, 1, STATE_ON, True),
+ (STATE_ON, True, 10, STATE_ON, 0, STATE_ON, True),
+ (STATE_ON, True, 20, STATE_OFF, 1, STATE_OFF, False),
+ (STATE_OFF, True, 20, STATE_OFF, 0, STATE_OFF, False),
+ ],
+)
+async def test_window_feature_manager_window_auto(
+ hass: HomeAssistant,
+ current_state,
+ in_cycle,
+ new_temp,
+ new_state, # new state of motion event
+ nb_call,
+ window_state,
+ is_window_detected,
+):
+ """Test the FeatureMotionManager class direclty"""
+
+ fake_vtherm = MagicMock(spec=BaseThermostat)
+ type(fake_vtherm).name = PropertyMock(return_value="the name")
+ type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
+ type(fake_vtherm).hvac_mode = PropertyMock(return_value=HVACMode.HEAT)
+ type(fake_vtherm).last_central_mode = PropertyMock(return_value=None)
+ type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=None)
+
+ # 1. creation / post_init / start listening
+ window_manager = FeatureWindowManager(fake_vtherm, hass)
+ window_manager.post_init(
+ {
+ CONF_USE_WINDOW_FEATURE: True,
+ CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
+ CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
+ CONF_WINDOW_AUTO_MAX_DURATION: 10,
+ CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
+ }
+ )
+ assert window_manager.is_window_auto_configured is True
+ window_manager.start_listening()
+
+ # 2. Call manage window auto
+ tz = get_tz(hass) # pylint: disable=invalid-name
+ now: datetime = datetime.now(tz=tz)
+
+ # Add a fake temp point for the window_auto_algo. We need at least 4 points
+ for i in range(0, 4):
+ window_manager._window_auto_algo.add_temp_measurement(
+ 17 + (i * (new_temp - 17) / 4), now, True
+ )
+ now = now + timedelta(minutes=5)
+
+ # fmt:off
+ with patch("custom_components.versatile_thermostat.feature_window_manager.FeatureWindowManager.update_window_state") as mock_update_window_state:
+ #fmt: on
+ now = now + timedelta(minutes=10)
+ # From 17 to new_temp in 10 minutes
+ type(fake_vtherm).ema_temperature = PropertyMock(return_value=new_temp)
+ type(fake_vtherm).last_temperature_measure = PropertyMock(return_value=now)
+ type(fake_vtherm).now = PropertyMock(return_value=now)
+ fake_vtherm.send_event = MagicMock()
+
+ window_manager._window_auto_state = current_state
+
+ dearm_window_auto = await window_manager.manage_window_auto(in_cycle=in_cycle)
+ assert dearm_window_auto is not None
+
+ assert mock_update_window_state.call_count == nb_call
+ if nb_call > 0:
+ mock_update_window_state.assert_has_calls(
+ [
+ call.update_window_state(new_state),
+ ]
+ )
+ if new_state == STATE_ON:
+ assert window_manager._window_call_cancel is not None
+
+ assert window_manager.window_auto_state == new_state
+ # update_window_state is mocked
+ # assert window_manager.window_state == new_state
+
+ window_manager.stop_listening()
+ await hass.async_block_till_done()