Tests ok. But tests are not complete

This commit is contained in:
Jean-Marc Collin
2024-12-26 20:00:51 +00:00
parent a11eaef9f7
commit 18248aee9e
12 changed files with 869 additions and 431 deletions

View File

@@ -69,7 +69,7 @@ motion_state: 'off'
overpowering_state: false overpowering_state: false
presence_state: 'on' presence_state: 'on'
window_auto_state: false window_auto_state: false
window_bypass_state: false is_window_bypass: false
security_delay_min: 2 security_delay_min: 2
security_min_on_percent: 0.5 security_min_on_percent: 0.5
security_default_on_percent: 0.1 security_default_on_percent: 0.1

View File

@@ -6,7 +6,6 @@ import math
import logging import logging
from typing import Any, Generic from typing import Any, Generic
from datetime import timedelta, datetime
from homeassistant.core import ( from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
@@ -26,11 +25,8 @@ from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_call_later,
) )
from homeassistant.exceptions import ConditionError
from homeassistant.helpers import condition
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_PRESET_MODE, ATTR_PRESET_MODE,
@@ -55,7 +51,6 @@ from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_OFF,
STATE_ON, STATE_ON,
) )
@@ -68,13 +63,13 @@ from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingEntity from .underlyings import UnderlyingEntity
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import ExponentialMovingAverage from .ema import ExponentialMovingAverage
from .base_manager import BaseFeatureManager from .base_manager import BaseFeatureManager
from .feature_presence_manager import FeaturePresenceManager from .feature_presence_manager import FeaturePresenceManager
from .feature_power_manager import FeaturePowerManager from .feature_power_manager import FeaturePowerManager
from .feature_motion_manager import FeatureMotionManager from .feature_motion_manager import FeatureMotionManager
from .feature_window_manager import FeatureWindowManager
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -115,13 +110,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"minimal_activation_delay_sec", "minimal_activation_delay_sec",
"last_update_datetime", "last_update_datetime",
"timezone", "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",
"temperature_unit", "temperature_unit",
"is_device_active", "is_device_active",
"device_actives", "device_actives",
@@ -168,10 +156,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._fan_mode = None self._fan_mode = None
self._humidity = None self._humidity = None
self._swing_mode = None self._swing_mode = None
self._window_state = None
self._saved_hvac_mode = None self._saved_hvac_mode = None
self._window_call_cancel = None
self._cur_temp = None self._cur_temp = None
self._ac_mode = None self._ac_mode = None
@@ -199,17 +185,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._underlying_climate_start_hvac_action_date = None self._underlying_climate_start_hvac_action_date = None
self._underlying_climate_delta_t = 0 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) 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 # Last change time is the datetime of the last change sent by VTherm to the device
@@ -246,10 +221,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
) )
self._power_manager: FeaturePowerManager = FeaturePowerManager(self, hass) self._power_manager: FeaturePowerManager = FeaturePowerManager(self, hass)
self._motion_manager: FeatureMotionManager = FeatureMotionManager(self, hass) self._motion_manager: FeatureMotionManager = FeatureMotionManager(self, hass)
self._window_manager: FeatureWindowManager = FeatureWindowManager(self, hass)
self.register_manager(self._presence_manager) self.register_manager(self._presence_manager)
self.register_manager(self._power_manager) self.register_manager(self._power_manager)
self.register_manager(self._motion_manager) self.register_manager(self._motion_manager)
self.register_manager(self._window_manager)
self.post_init(entry_infos) self.post_init(entry_infos)
@@ -338,10 +315,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_preset_modes: list[str] | None self._attr_preset_modes: list[str] | None
if self._window_call_cancel is not None:
self._window_call_cancel()
self._window_call_cancel = None
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN) self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
# Initialize underlying entities (will be done in subclasses) # Initialize underlying entities (will be done in subclasses)
@@ -353,29 +326,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
CONF_LAST_SEEN_TEMP_SENSOR CONF_LAST_SEEN_TEMP_SENSOR
) )
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR) self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_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._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT) self._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT)
self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT) self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT)
@@ -441,9 +391,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if self._prop_algorithm is not None: if self._prop_algorithm is not None:
del self._prop_algorithm del self._prop_algorithm
# Memory synthesis state
self._window_state = None
self._total_energy = None self._total_energy = None
_LOGGER.debug("%s - post_init_ resetting energy to None", self) _LOGGER.debug("%s - post_init_ resetting energy to None", self)
@@ -470,10 +417,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entry_infos.get(CONF_USED_BY_CENTRAL_BOILER) is True 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 self._max_on_percent = api.max_on_percent
_LOGGER.debug( _LOGGER.debug(
@@ -514,15 +457,6 @@ 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,
)
)
# start listening for all managers # start listening for all managers
for manager in self._managers: for manager in self._managers:
manager.start_listening() manager.start_listening()
@@ -604,21 +538,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self, self,
) )
# 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
# refresh states for all managers # refresh states for all managers
for manager in self._managers: for manager in self._managers:
if await manager.refresh_state(): if await manager.refresh_state():
@@ -692,7 +611,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
]: ]:
self._hvac_mode = old_state.state 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_hvac_mode = old_state.attributes.get("saved_hvac_mode", None)
self._saved_preset_mode = old_state.attributes.get( self._saved_preset_mode = old_state.attributes.get(
"saved_preset_mode", None "saved_preset_mode", None
@@ -730,7 +649,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if not self.is_on and self.hvac_off_reason is None: if not self.is_on and self.hvac_off_reason is None:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL) 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.PRESET_EVENT, {"preset": self._attr_preset_mode})
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@@ -938,22 +857,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property @property
def window_state(self) -> str | None: def window_state(self) -> str | None:
"""Get the window_state""" """Get the window_state"""
return STATE_ON if self._window_state else STATE_OFF return self._window_manager.window_state
@property @property
def window_auto_state(self) -> str | None: def window_auto_state(self) -> str | None:
"""Get the window_auto_state""" """Get the window_auto_state"""
return STATE_ON if self._window_auto_state else STATE_OFF return self._window_manager.window_auto_state
@property @property
def window_bypass_state(self) -> bool | None: def is_window_bypass(self) -> bool | None:
"""Get the Window Bypass""" """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
@property @property
def security_state(self) -> bool | None: def security_state(self) -> bool | None:
@@ -1000,15 +914,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property @property
def last_temperature_slope(self) -> float | None: def last_temperature_slope(self) -> float | None:
"""Return the last temperature slope curve if any""" """Return the last temperature slope curve if any"""
if not self._window_auto_algo: return self._window_manager.last_slope
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
@property @property
def nb_underlying_entities(self) -> int: def nb_underlying_entities(self) -> int:
@@ -1213,16 +1119,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if preset_mode == PRESET_NONE: if preset_mode == PRESET_NONE:
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
if self._saved_target_temp: if self._saved_target_temp:
await self.change_target_temperature(self._saved_target_temp) await self.restore_target_temp()
elif preset_mode == PRESET_ACTIVITY: elif preset_mode == PRESET_ACTIVITY:
self._attr_preset_mode = PRESET_ACTIVITY self._attr_preset_mode = PRESET_ACTIVITY
await self._motion_manager.update_motion(None, False) await self._motion_manager.update_motion_state(None, False)
else: else:
if self._attr_preset_mode == PRESET_NONE: if self._attr_preset_mode == PRESET_NONE:
self._saved_target_temp = self._target_temp self.save_target_temp()
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
# Switch the temperature if window is not 'on' # Switch the temperature if window is not 'on'
if self.window_state != STATE_ON: if not self._window_manager.is_window_detected:
await self.change_target_temperature(self.find_preset_temp(preset_mode)) await self.change_target_temperature(self.find_preset_temp(preset_mode))
else: else:
# Window is on, so we just save the new expected temp # Window is on, so we just save the new expected temp
@@ -1339,7 +1245,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return return
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
if self.window_state != STATE_ON: if not self._window_manager.is_window_detected:
await self.change_target_temperature(temperature) await self.change_target_temperature(temperature)
self.recalculate() self.recalculate()
self.reset_last_change_time_from_vtherm() self.reset_last_change_time_from_vtherm()
@@ -1376,8 +1282,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data) _LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data)
@callback @callback
async def _async_temperature_changed(self, event: Event): async def _async_temperature_changed(self, event: Event) -> callable:
"""Handle temperature of the temperature sensor changes.""" """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") new_state: State = event.data.get("new_state")
_LOGGER.debug( _LOGGER.debug(
"%s - Temperature changed. Event.new_state is %s", "%s - Temperature changed. Event.new_state is %s",
@@ -1387,6 +1294,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return return
# TODO ce code avec du dearm est curieux. A voir après refacto
dearm_window_auto = await self._async_update_temp(new_state) dearm_window_auto = await self._async_update_temp(new_state)
self.recalculate() self.recalculate()
await self.async_control_heating(force=False) await self.async_control_heating(force=False)
@@ -1446,70 +1354,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.recalculate() self.recalculate()
await self.async_control_heating(force=False) 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 @callback
async def _check_initial_state(self): async def _check_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF.""" """Prevent the device from keep running if HVAC_MODE_OFF."""
@@ -1518,17 +1362,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await under.check_initial_state(self._hvac_mode) await under.check_initial_state(self._hvac_mode)
# Prevent from starting a VTherm if window is open # Prevent from starting a VTherm if window is open
if ( if self.is_on:
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
):
_LOGGER.info("%s - the window is open. Prevent starting the VTherm") _LOGGER.info("%s - the window is open. Prevent starting the VTherm")
self._window_auto_state = True await self._window_manager.refresh_state()
self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF)
# Starts the initial control loop (don't wait for an update of temperature) # Starts the initial control loop (don't wait for an update of temperature)
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
@@ -1561,7 +1397,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self.check_safety() await self.check_safety()
# check window_auto # check window_auto
return await self._async_manage_window_auto() return await self._window_manager.manage_window_auto()
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update temperature from sensor: %s", ex) _LOGGER.error("Unable to update temperature from sensor: %s", ex)
@@ -1595,111 +1431,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
for under in self._underlyings: for under in self._underlyings:
await under.turn_off() 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): def save_preset_mode(self):
"""Save the current preset mode to be restored later """Save the current preset mode to be restored later
We never save a hidden preset mode We never save a hidden preset mode
@@ -1744,6 +1475,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_mode, self._hvac_mode,
) )
def save_target_temp(self):
"""Save the target temperature"""
self._saved_target_temp = self._target_temp
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( async def check_central_mode(
self, new_central_mode: str | None, old_central_mode: str | None self, new_central_mode: str | None, old_central_mode: str | None
): ):
@@ -1768,16 +1507,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.save_preset_mode() self.save_preset_mode()
self.save_hvac_mode() self.save_hvac_mode()
is_window_detected = self._window_manager.is_window_detected
if new_central_mode == CENTRAL_MODE_AUTO: 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_hvac_mode()
await self.restore_preset_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 # do not restore but mark the reason of off with window detection
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION) self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
return 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() save_all()
if new_central_mode == CENTRAL_MODE_STOPPED: if new_central_mode == CENTRAL_MODE_STOPPED:
@@ -1990,78 +1730,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
should have found the underlying climate to be operational""" should have found the underlying climate to be operational"""
return True 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.change_target_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.change_target_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.change_target_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: async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
@@ -2074,7 +1742,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
) )
# check auto_window conditions # check auto_window conditions
await self._async_manage_window_auto(in_cycle=True) await self._window_manager.manage_window_auto(in_cycle=True)
# 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.is_initialized:
@@ -2160,16 +1828,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"saved_preset_mode": self._saved_preset_mode, "saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp, "saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode, "saved_hvac_mode": self._saved_hvac_mode,
"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_delay_min": self._security_delay_min,
"security_min_on_percent": self._security_min_on_percent, "security_min_on_percent": self._security_min_on_percent,
"security_default_on_percent": self._security_default_on_percent, "security_default_on_percent": self._security_default_on_percent,
@@ -2215,6 +1873,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""True if the Thermostat is regulated by valve""" """True if the Thermostat is regulated by valve"""
return False 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 @callback
def async_registry_entry_updated(self): def async_registry_entry_updated(self):
"""update the entity if the config entry have been updated """update the entity if the config entry have been updated
@@ -2324,22 +1997,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self, self,
window_bypass, window_bypass,
) )
self._window_bypass_state = window_bypass if await self._window_manager.set_window_bypass(window_bypass):
if not self._window_bypass_state and self._window_state: self.update_custom_attributes()
_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()
def send_event(self, event_type: EventType, data: dict): def send_event(self, event_type: EventType, data: dict):
"""Send an event""" """Send an event"""
@@ -2428,3 +2087,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self.async_set_hvac_mode(HVACMode.COOL) await self.async_set_hvac_mode(HVACMode.COOL)
else: else:
await self.async_set_hvac_mode(HVACMode.HEAT) 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

View File

@@ -317,8 +317,8 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
"""Called when my climate have change""" """Called when my climate have change"""
# _LOGGER.debug("%s - climate state change", self._attr_unique_id) # _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on old_state = self._attr_is_on
if self.my_climate.window_bypass_state in [True, False]: if self.my_climate.is_window_bypass in [True, False]:
self._attr_is_on = self.my_climate.window_bypass_state self._attr_is_on = self.my_climate.is_window_bypass
if old_state != self._attr_is_on: if old_state != self._attr_is_on:
self.async_write_ha_state() self.async_write_ha_state()
return return

View File

@@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__)
class FeatureMotionManager(BaseFeatureManager): class FeatureMotionManager(BaseFeatureManager):
"""The implementation of the Presence feature""" """The implementation of the Motion feature"""
unrecorded_attributes = frozenset( unrecorded_attributes = frozenset(
{ {
@@ -67,9 +67,7 @@ class FeatureMotionManager(BaseFeatureManager):
@overrides @overrides
def post_init(self, entry_infos: ConfigData): def post_init(self, entry_infos: ConfigData):
"""Reinit of the manager""" """Reinit of the manager"""
if self._motion_call_cancel is not None: self.dearm_motion_timer()
self._motion_call_cancel() # pylint: disable="not-callable"
self._motion_call_cancel = None
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR, None) 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_delay_sec = entry_infos.get(CONF_MOTION_DELAY, 0)
@@ -130,7 +128,7 @@ class FeatureMotionManager(BaseFeatureManager):
self._motion_state, self._motion_state,
) )
# recalculate the right target_temp in activity mode # recalculate the right target_temp in activity mode
ret = await self.update_motion(motion_state.state, False) ret = await self.update_motion_state(motion_state.state, False)
return ret return ret
@@ -193,9 +191,9 @@ class FeatureMotionManager(BaseFeatureManager):
if long_enough: if long_enough:
_LOGGER.debug("%s - Motion delay condition is satisfied", self) _LOGGER.debug("%s - Motion delay condition is satisfied", self)
await self.update_motion(new_state.state) await self.update_motion_state(new_state.state)
else: else:
await self.update_motion( await self.update_motion_state(
STATE_ON if new_state.state == STATE_OFF else STATE_OFF STATE_ON if new_state.state == STATE_OFF else STATE_OFF
) )
@@ -242,10 +240,10 @@ class FeatureMotionManager(BaseFeatureManager):
_LOGGER.debug("%s - Event ignored cause i'm already on", self) _LOGGER.debug("%s - Event ignored cause i'm already on", self)
return None return None
async def update_motion( async def update_motion_state(
self, new_state: str = None, recalculate: bool = True self, new_state: str = None, recalculate: bool = True
) -> bool: ) -> bool:
"""Update the value of the presence sensor and update the VTherm state accordingly """Update the value of the motion sensor and update the VTherm state accordingly
Return true if a change has been made""" Return true if a change has been made"""
_LOGGER.info("%s - Updating motion state. New state is %s", self, new_state) _LOGGER.info("%s - Updating motion state. New state is %s", self, new_state)
@@ -261,7 +259,7 @@ class FeatureMotionManager(BaseFeatureManager):
new_preset, new_preset,
) )
# We do not change the preset which is kept to ACTIVITY but only the target_temperature # We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account # We take the motion into account
new_temp = self._vtherm.find_preset_temp(new_preset) new_temp = self._vtherm.find_preset_temp(new_preset)
old_temp = self._vtherm.target_temperature old_temp = self._vtherm.target_temperature
if new_temp != old_temp: if new_temp != old_temp:
@@ -298,12 +296,12 @@ class FeatureMotionManager(BaseFeatureManager):
@overrides @overrides
@property @property
def is_configured(self) -> bool: def is_configured(self) -> bool:
"""Return True of the presence is configured""" """Return True of the motion is configured"""
return self._is_configured return self._is_configured
@property @property
def motion_state(self) -> str | None: def motion_state(self) -> str | None:
"""Return the current presence state STATE_ON or STATE_OFF """Return the current motion state STATE_ON or STATE_OFF
or STATE_UNAVAILABLE if not configured""" or STATE_UNAVAILABLE if not configured"""
if not self._is_configured: if not self._is_configured:
return STATE_UNAVAILABLE return STATE_UNAVAILABLE
@@ -311,14 +309,14 @@ class FeatureMotionManager(BaseFeatureManager):
@property @property
def is_motion_detected(self) -> bool: def is_motion_detected(self) -> bool:
"""Return true if the presence is configured and presence sensor is OFF""" """Return true if the motion is configured and motion sensor is OFF"""
return self._is_configured and self._motion_state in [ return self._is_configured and self._motion_state in [
STATE_ON, STATE_ON,
] ]
@property @property
def motion_sensor_entity_id(self) -> bool: def motion_sensor_entity_id(self) -> bool:
"""Return true if the presence is configured and presence sensor is OFF""" """Return true if the motion is configured and motion sensor is OFF"""
return self._motion_sensor_entity_id return self._motion_sensor_entity_id
@property @property

View File

@@ -0,0 +1,553 @@
""" 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_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:
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 == STATE_ON
return
_LOGGER.debug("%s - Window delay condition is satisfied", self)
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._is_window_bypass)
if self._is_window_bypass:
_LOGGER.info(
"%s - Window ByPass is activated. Ignore window event", self
)
else:
await self.update_window_state(self._window_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
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
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,
)
self._window_state = new_state
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,
)
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"
)
return
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)
is not None
):
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) is not None
):
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)
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 = False
await self.update_window_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._vtherm.ema_temp,
datetime_now=self._vtherm.now,
)
else:
slope = self._window_auto_algo.add_temp_measurement(
temperature=self._vtherm.ema_temp,
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
if (
self._window_auto_algo.is_window_open_detected()
and self._window_auto_state is False
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 = True
await self.update_window_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 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.is_configured:
return None
return self._window_auto_algo.last_slope
def __str__(self):
return f"WindowManager-{self.name}"

View File

@@ -1126,7 +1126,7 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset | | ``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 | | ``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_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é | | ``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 | | ``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 | | ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |

View File

@@ -248,7 +248,7 @@ The custom attributes are as follows:
| ``saved_preset_mode`` | The last preset used before automatic preset switching | | ``saved_preset_mode`` | The last preset used before automatic preset switching |
| ``saved_target_temp`` | The last temperature used before automatic 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_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 | | ``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 | | ``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 | | ``presence_state`` | The last known state of the presence sensor. None if presence detection is not configured |

View File

@@ -1126,7 +1126,7 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset | | ``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 | | ``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_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é | | ``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 | | ``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 | | ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |

View File

@@ -247,7 +247,7 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset | | ``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 | | ``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_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é | | ``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 | | ``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 | | ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |

View File

@@ -167,7 +167,7 @@ async def test_minimal_over_switch_wo_central_config(
assert entity.max_temp == 18 assert entity.max_temp == 18
assert entity.target_temperature_step == 0.3 assert entity.target_temperature_step == 0.3
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"] assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
assert entity.is_window_auto_enabled is False assert entity.is_window_auto_configured is False
assert entity.nb_underlying_entities == 1 assert entity.nb_underlying_entities == 1
assert entity.underlying_entity_id(0) == "switch.mock_switch" assert entity.underlying_entity_id(0) == "switch.mock_switch"
assert entity.proportional_algorithm is not None assert entity.proportional_algorithm is not None
@@ -279,7 +279,7 @@ async def test_full_over_switch_wo_central_config(
assert entity._security_default_on_percent == 0.1 assert entity._security_default_on_percent == 0.1
assert entity.is_inversed is False assert entity.is_inversed is False
assert entity.is_window_auto_enabled is False # we have an entity_id assert entity.is_window_auto_configured is False # we have an entity_id
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor" assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
assert entity._window_delay_sec == 30 assert entity._window_delay_sec == 30
assert entity._window_auto_close_threshold == 0.1 assert entity._window_auto_close_threshold == 0.1
@@ -402,7 +402,7 @@ async def test_full_over_switch_with_central_config(
assert entity.is_inversed is False assert entity.is_inversed is False
# We have an entity so window auto is not enabled # We have an entity so window auto is not enabled
assert entity.is_window_auto_enabled is False assert entity.is_window_auto_configured is False
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor" assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
assert entity._window_delay_sec == 15 assert entity._window_delay_sec == 15
assert entity._window_auto_close_threshold == 1 assert entity._window_auto_close_threshold == 1

View File

@@ -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 Window management """ """ Test the Window management """
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@@ -2,18 +2,242 @@
""" Test the Window management """ """ Test the Window management """
import asyncio import asyncio
import logging import logging
from unittest.mock import patch, call, PropertyMock from unittest.mock import patch, call, PropertyMock, AsyncMock, MagicMock
from datetime import datetime, timedelta from datetime import datetime, timedelta
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import ( from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate, ThermostatOverClimate,
) )
from custom_components.versatile_thermostat.feature_window_manager import (
FeatureWindowManager,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) 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_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_TURN_OFF, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ),
# delay is missing
( True, "sensor.the_window_sensor", None, None, None, None, CONF_WINDOW_TURN_OFF, 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_TURN_OFF, 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_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ),
# With Window auto config not complete -> missing close threshold
( True, None, None, 1, None, 3, CONF_WINDOW_TURN_OFF, 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),
],
)
async def test_window_feature_manager_refresh_sensor(
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,
}
)
# 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()
# force old state for the test
window_manager._window_state = current_state
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 mock_get_state.call_count == 1
assert fake_vtherm.async_set_hvac_mode.call_count == nb_call
assert fake_vtherm.set_hvac_off_reason.call_count == nb_call
if nb_call == 1:
fake_vtherm.async_set_hvac_mode.assert_has_calls(
[
call.async_set_hvac_mode(HVACMode.OFF),
]
)
fake_vtherm.set_hvac_off_reason.assert_has_calls(
[
call.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION),
]
)
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("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_not_enough( async def test_window_management_time_not_enough(
@@ -308,7 +532,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is True assert entity.is_window_auto_configured is True
# Initialize the slope algo with 2 measurements # Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -505,7 +729,7 @@ async def test_window_auto_fast_and_sensor(
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is False assert entity.is_window_auto_configured is False
# Initialize the slope algo with 2 measurements # Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -621,7 +845,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is True assert entity.is_window_auto_configured is True
# 1. Initialize the slope algo with 2 measurements # 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -895,7 +1119,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is False assert entity.is_window_auto_configured is False
# change temperature to force turning on the heater # change temperature to force turning on the heater
with patch( with patch(
@@ -918,9 +1142,9 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
# Set Window ByPass to true # Set Window ByPass to true
await entity.service_set_window_bypass_state(True) await entity.service_set_window_bypass_state(True)
assert entity.window_bypass_state is True assert entity.is_window_bypass is True
# entity._window_bypass_state = True # entity._is_window_bypass = True
# Open the window, condition of time is satisfied, check the thermostat and heater turns off # Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch( with patch(
@@ -1038,7 +1262,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled assert entity.is_window_auto_configured
# Initialize the slope algo with 2 measurements # Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -1072,7 +1296,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
# send one degre down in one minute with window bypass on # send one degre down in one minute with window bypass on
await entity.service_set_window_bypass_state(True) await entity.service_set_window_bypass_state(True)
assert entity.window_bypass_state is True assert entity.is_window_bypass is True
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
@@ -1595,7 +1819,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is True assert entity.is_window_auto_configured is True
# 1. Initialize the slope algo with 2 measurements # 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -1792,7 +2016,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is True assert entity.is_window_auto_configured is True
# 1. Initialize the slope algo with 2 measurements # 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -2151,7 +2375,7 @@ async def test_window_action_frost_temp_preset_change(
assert vtherm.target_temperature == 21 assert vtherm.target_temperature == 21
assert vtherm.window_state is STATE_OFF assert vtherm.window_state is STATE_OFF
assert vtherm.is_window_auto_enabled is False assert vtherm.is_window_auto_configured is False
# 1. Turn on the window sensor # 1. Turn on the window sensor
now = now + timedelta(minutes=1) now = now + timedelta(minutes=1)
@@ -2261,7 +2485,7 @@ async def test_window_action_frost_temp_temp_change(
assert vtherm.target_temperature == 21 assert vtherm.target_temperature == 21
assert vtherm.window_state is STATE_OFF assert vtherm.window_state is STATE_OFF
assert vtherm.is_window_auto_enabled is False assert vtherm.is_window_auto_configured is False
# 1. Turn on the window sensor # 1. Turn on the window sensor
now = now + timedelta(minutes=1) now = now + timedelta(minutes=1)