Compare commits
4 Commits
6.5.0.beta
...
6.5.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f547647e24 | ||
|
|
5063374b97 | ||
|
|
461db8d86c | ||
|
|
38c4b067b1 |
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
@@ -13,7 +13,6 @@ from .const import (
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
CONF_AUTO_START_STOP_LEVELS,
|
||||
TYPE_AUTO_START_STOP_LEVELS,
|
||||
)
|
||||
|
||||
@@ -31,6 +30,9 @@ DT_MIN = {
|
||||
# the measurement cycle (2 min)
|
||||
CYCLE_SEC = 120
|
||||
|
||||
# A temp hysteresis to avoid rapid OFF/ON
|
||||
TEMP_HYSTERESIS = 0.5
|
||||
|
||||
ERROR_THRESHOLD = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
|
||||
@@ -79,7 +81,7 @@ class AutoStartStopDetectionAlgorithm:
|
||||
saved_hvac_mode: HVACMode | None,
|
||||
target_temp: float,
|
||||
current_temp: float,
|
||||
slope_min: float,
|
||||
slope_min: float | None,
|
||||
now: datetime,
|
||||
) -> AUTO_START_STOP_ACTIONS:
|
||||
"""Calculate an eventual action to do depending of the value in parameter"""
|
||||
@@ -90,18 +92,6 @@ class AutoStartStopDetectionAlgorithm:
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if (
|
||||
hvac_mode is None
|
||||
or target_temp is None
|
||||
or current_temp is None
|
||||
or slope_min is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - No all mandatory parameters are set. Disable auto-start/stop",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s",
|
||||
self,
|
||||
@@ -113,14 +103,21 @@ class AutoStartStopDetectionAlgorithm:
|
||||
now,
|
||||
)
|
||||
|
||||
if hvac_mode is None or target_temp is None or current_temp is None:
|
||||
_LOGGER.debug(
|
||||
"%s - No all mandatory parameters are set. Disable auto-start/stop",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = target_temp - current_temp
|
||||
|
||||
# reduce the error considering the dt between the last measurement
|
||||
if self._last_calculation_date is not None:
|
||||
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
|
||||
# ignore two calls too near (< 2,5 min)
|
||||
if dtmin <= 0.5:
|
||||
# ignore two calls too near (< 24 sec)
|
||||
if dtmin <= 0.2:
|
||||
_LOGGER.debug(
|
||||
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
|
||||
self,
|
||||
@@ -144,59 +141,73 @@ class AutoStartStopDetectionAlgorithm:
|
||||
|
||||
self._last_calculation_date = now
|
||||
|
||||
temp_at_dt = current_temp + slope_min * self._dt
|
||||
|
||||
# Check to turn-off
|
||||
# When we hit the threshold, that mean we can turn off
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
if self._accumulated_error <= -self._error_threshold:
|
||||
if (
|
||||
self._accumulated_error <= -self._error_threshold
|
||||
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for heating for a long time.",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we are heating",
|
||||
)
|
||||
_LOGGER.debug("%s - nothing to do, we are heating", self)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
if self._accumulated_error >= self._error_threshold:
|
||||
if (
|
||||
self._accumulated_error >= self._error_threshold
|
||||
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for cooling for a long time.",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we are cooling",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# check to turn on
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
|
||||
if current_temp + slope_min * self._dt <= target_temp:
|
||||
if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to heat",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we don't need to heat soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
|
||||
if current_temp + slope_min * self._dt >= target_temp:
|
||||
if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to cool",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we don't need to cool soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, no conditions applied",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
@@ -214,6 +225,11 @@ class AutoStartStopDetectionAlgorithm:
|
||||
"""Get the accumulated error value"""
|
||||
return self._accumulated_error
|
||||
|
||||
@property
|
||||
def accumulated_error_threshold(self) -> float:
|
||||
"""Get the accumulated error threshold value"""
|
||||
return self._error_threshold
|
||||
|
||||
@property
|
||||
def level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Get the level value"""
|
||||
|
||||
@@ -199,6 +199,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"is_device_active",
|
||||
"target_temperature_step",
|
||||
"is_used_by_central_boiler",
|
||||
"temperature_slope"
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -2633,6 +2634,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"is_device_active": self.is_device_active,
|
||||
"ema_temp": self._ema_temp,
|
||||
"is_used_by_central_boiler": self.is_used_by_central_boiler,
|
||||
"temperature_slope": round(self.last_temperature_slope or 0, 3),
|
||||
}
|
||||
|
||||
@callback
|
||||
|
||||
@@ -100,7 +100,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the SecurityState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Security state"
|
||||
self._attr_unique_id = f"{self._device_name}_security_state"
|
||||
self._attr_is_on = False
|
||||
|
||||
@@ -52,6 +52,7 @@ PLATFORMS: list[Platform] = [
|
||||
# Number should be after CLIMATE
|
||||
Platform.NUMBER,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
|
||||
102
custom_components/versatile_thermostat/switch.py
Normal file
102
custom_components/versatile_thermostat/switch.py
Normal file
@@ -0,0 +1,102 @@
|
||||
## pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
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 .const import * # pylint: disable=unused-wildcard-import,wildcard-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat switches with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE and auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
async_add_entities([enable_entity], True)
|
||||
|
||||
|
||||
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
||||
"""The that enables the ManagedDevice optimisation with"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
|
||||
):
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Enable auto start/stop"
|
||||
self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
|
||||
self._default_value = (
|
||||
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
|
||||
!= AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:power-settings"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Récupérer le dernier état sauvegardé de l'entité
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
# Si l'état précédent existe, vous pouvez l'utiliser
|
||||
if last_state is not None:
|
||||
self._attr_is_on = last_state.state == "on"
|
||||
else:
|
||||
# If no previous state set it to false by default
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
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)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
self.turn_on()
|
||||
|
||||
@callback
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.turn_off()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
self._attr_is_on = False
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
self._attr_is_on = True
|
||||
self.update_my_state_and_vtherm()
|
||||
@@ -49,6 +49,7 @@ from .const import (
|
||||
RegulationParamStrong,
|
||||
AUTO_FAN_DTEMP_THRESHOLD,
|
||||
AUTO_FAN_DEACTIVATED_MODES,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE,
|
||||
CONF_AUTO_START_STOP_LEVEL,
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
TYPE_AUTO_START_STOP_LEVELS,
|
||||
@@ -91,6 +92,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
_auto_deactivated_fan_mode: str | None = None
|
||||
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
|
||||
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
_is_auto_start_stop_enabled: bool = False
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
@@ -110,8 +112,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"auto_deactivated_fan_mode",
|
||||
"auto_regulation_use_device_temp",
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtemp",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -173,9 +177,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
self._auto_start_stop_level = config_entry.get(
|
||||
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
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
|
||||
@@ -572,12 +581,23 @@ 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.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -898,56 +918,67 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
# Check if we need to auto start/stop the Vtherm
|
||||
action = self._auto_start_stop_algo.calculate_action(
|
||||
self.hvac_mode,
|
||||
self._saved_hvac_mode,
|
||||
self.target_temperature,
|
||||
self.current_temperature,
|
||||
self._window_auto_algo.last_slope,
|
||||
self.now
|
||||
)
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions", self
|
||||
if self.auto_start_stop_enable:
|
||||
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,
|
||||
)
|
||||
await self.async_turn_off()
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
|
||||
self,
|
||||
)
|
||||
await self.async_turn_off()
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"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": self._window_auto_algo.last_slope,
|
||||
},
|
||||
)
|
||||
# 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),
|
||||
},
|
||||
)
|
||||
|
||||
# Stop here
|
||||
return ret
|
||||
elif action == AUTO_START_STOP_ACTION_ON:
|
||||
_LOGGER.info(
|
||||
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
|
||||
)
|
||||
await self.async_turn_on()
|
||||
# Stop here
|
||||
return ret
|
||||
elif action == AUTO_START_STOP_ACTION_ON:
|
||||
_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",
|
||||
"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": self._window_auto_algo.last_slope,
|
||||
},
|
||||
)
|
||||
# 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),
|
||||
},
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
else:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled")
|
||||
|
||||
# Continue the normal async_control_heating
|
||||
|
||||
@@ -959,6 +990,11 @@ 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
|
||||
self.update_custom_attributes()
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self) -> str | None:
|
||||
"""Get the regulation mode"""
|
||||
@@ -1110,6 +1146,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""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
|
||||
|
||||
@overrides
|
||||
def init_underlyings(self):
|
||||
"""Init the underlyings if not already done"""
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
@@ -14,7 +15,6 @@ from custom_components.versatile_thermostat.auto_start_stop_algorithm import (
|
||||
AutoStartStopDetectionAlgorithm,
|
||||
AUTO_START_STOP_ACTION_NOTHING,
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -72,7 +72,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
|
||||
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# 4 .No change on accumulated error because the new measure is too near the last one
|
||||
now = now + timedelta(minutes=1)
|
||||
now = now + timedelta(seconds=11)
|
||||
ret = algo.calculate_action(
|
||||
hvac_mode=HVACMode.HEAT,
|
||||
saved_hvac_mode=HVACMode.OFF,
|
||||
@@ -225,6 +225,7 @@ async def test_auto_start_stop_none_vtherm(
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
@@ -267,6 +268,12 @@ async def test_auto_start_stop_none_vtherm(
|
||||
# 1. Vtherm auto-start/stop should be in NONE mode
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
# 2. We should not find any switch Enable entity
|
||||
assert (
|
||||
search_entity(hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@@ -310,6 +317,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
@@ -350,8 +358,13 @@ 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
|
||||
# 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
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
assert enable_entity is not None
|
||||
assert enable_entity.state == STATE_ON
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -369,6 +382,8 @@ 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
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -421,12 +436,13 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": "overClimate",
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": HVACMode.OFF,
|
||||
"saved_hvac_mode": HVACMode.HEAT,
|
||||
"target_temperature": 19.0,
|
||||
"current_temperature": 21.0,
|
||||
"temperature_slope": 10.03,
|
||||
"temperature_slope": 0.167,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -480,12 +496,13 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": "overClimate",
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": HVACMode.HEAT,
|
||||
"saved_hvac_mode": HVACMode.HEAT, # saved don't change
|
||||
"target_temperature": 19.0,
|
||||
"current_temperature": 18.0,
|
||||
"temperature_slope": -2.06,
|
||||
"temperature_slope": -0.034,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -545,6 +562,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
@@ -599,11 +617,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 25.0
|
||||
# VTherm should be heating
|
||||
# VTherm should be cooling
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
# 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
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -611,7 +631,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
await send_temperature_change_event(vtherm, 25, now, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# VTherm should still be heating
|
||||
# VTherm should still be cooling
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
assert mock_send_event.call_count == 0
|
||||
assert (
|
||||
@@ -641,12 +661,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": "overClimate",
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": HVACMode.OFF,
|
||||
"saved_hvac_mode": HVACMode.COOL,
|
||||
"target_temperature": 25.0,
|
||||
"current_temperature": 23.0,
|
||||
"temperature_slope": -16.8,
|
||||
"temperature_slope": -0.28,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -664,18 +685,18 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
)
|
||||
|
||||
# 5. Set temperature to over the target, but slope is too low -> no change
|
||||
now = now + timedelta(minutes=20)
|
||||
now = now + timedelta(minutes=30)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
vtherm._set_now(now)
|
||||
await send_temperature_change_event(vtherm, 26, now, True)
|
||||
await send_temperature_change_event(vtherm, 25.5, now, True)
|
||||
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
|
||||
|
||||
# VTherm should have been stopped
|
||||
# VTherm should stay stopped
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
# a message should have been sent
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -702,12 +723,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": "overClimate",
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": HVACMode.COOL,
|
||||
"saved_hvac_mode": HVACMode.COOL, # saved don't change
|
||||
"target_temperature": 25.0,
|
||||
"current_temperature": 26.5,
|
||||
"temperature_slope": 5.74,
|
||||
"temperature_slope": 0.112,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -767,6 +789,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
@@ -826,6 +849,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
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -846,12 +870,13 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": "overClimate",
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": HVACMode.OFF,
|
||||
"saved_hvac_mode": HVACMode.HEAT,
|
||||
"target_temperature": 17.0,
|
||||
"current_temperature": 19.0,
|
||||
"temperature_slope": 18.0,
|
||||
"temperature_slope": 0.3,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -900,12 +925,13 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": "overClimate",
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": HVACMode.HEAT,
|
||||
"saved_hvac_mode": HVACMode.HEAT, # saved don't change
|
||||
"target_temperature": 21.0,
|
||||
"current_temperature": 17.0,
|
||||
"temperature_slope": -5.19,
|
||||
"temperature_slope": -0.087,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -921,3 +947,136 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test than auto-start/stop restart a VTherm stopped upon preset_change (in fast mode)"""
|
||||
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# The temperatures to set
|
||||
temps = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"eco_ac": 27.0,
|
||||
"comfort_ac": 25.0,
|
||||
"boost_ac": 23.0,
|
||||
"frost_away": 7.1,
|
||||
"eco_away": 17.1,
|
||||
"comfort_away": 19.1,
|
||||
"boost_away": 21.1,
|
||||
"eco_ac_away": 27.1,
|
||||
"comfort_ac_away": 25.1,
|
||||
"boost_ac_away": 23.1,
|
||||
}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="overClimateUniqueId",
|
||||
data={
|
||||
CONF_NAME: "overClimate",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
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_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
},
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mock_climate",
|
||||
name="mock_climate",
|
||||
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
||||
hass, config_entry, "climate.overclimate"
|
||||
)
|
||||
|
||||
assert vtherm is not None
|
||||
|
||||
# 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_FAST
|
||||
)
|
||||
|
||||
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
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
assert enable_entity is not None
|
||||
assert enable_entity.state == STATE_ON
|
||||
|
||||
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_enable") is True
|
||||
|
||||
# 2. set enable to false
|
||||
enable_entity.turn_off()
|
||||
await hass.async_block_till_done()
|
||||
assert enable_entity.state == STATE_OFF
|
||||
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_enable") is False
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 3. Set mode to Heat and preset to Comfort
|
||||
await send_presence_change_event(vtherm, True, False, now)
|
||||
await send_temperature_change_event(vtherm, 16, now, True)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await vtherm.async_set_preset_mode(PRESET_ECO)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 17.0
|
||||
# VTherm should be heating
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 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
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
vtherm._set_now(now)
|
||||
await send_temperature_change_event(vtherm, 19, now, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# VTherm should not have been stopped cause enable is false
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# Not calculated cause enable = false
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == 0
|
||||
|
||||
# a message should have been sent
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
Reference in New Issue
Block a user