Feature 181 & 242 - improve auto window detection (#243)
* Add ema calculation class, calculate an emo temperature, use the ema_temperature in auto_window dectection * Removes circular dependency error * Fix ema_temp unknown and remove slope smoothing * 15 sec between two slope calculation * Take Maia feedbacks on the algo. * Maia comments: change MAX_ALPHA to 0.5, add slope calculation at each cycle. * With EMA entity and slope calculation optimisations * Change open_window_detection fake datapoint threshold * Try auto window new algo * Don't store datetime of fake datapoint * Change auto window threshold in °/hour --------- Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
@@ -113,10 +113,17 @@ from .underlyings import UnderlyingEntity
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from .ema import ExponentialMovingAverage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Representation of a base class for all Versatile Thermostat device."""
|
||||
|
||||
@@ -246,6 +253,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._underlyings = []
|
||||
|
||||
self._ema_temp = None
|
||||
self._ema_algo = None
|
||||
self.post_init(entry_infos)
|
||||
|
||||
def post_init(self, entry_infos):
|
||||
@@ -450,6 +459,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._total_energy = 0
|
||||
|
||||
self._ema_algo = ExponentialMovingAverage(
|
||||
self.name,
|
||||
self._cycle_min * 60,
|
||||
# Needed for time calculation
|
||||
get_tz(self._hass),
|
||||
# two digits after the coma for temperature slope calculation
|
||||
2,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||
self,
|
||||
@@ -862,6 +880,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def ema_temperature(self) -> str:
|
||||
"""Return the EMA temperature."""
|
||||
return self._ema_temp
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return current operation."""
|
||||
@@ -1476,6 +1499,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._last_temperature_mesure = self.get_state_date_or_now(state)
|
||||
|
||||
# calculate the smooth_temperature with EMA calculation
|
||||
self._ema_temp = self._ema_algo.calculate_ema(
|
||||
self._cur_temp, self._last_temperature_mesure
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - After setting _last_temperature_mesure %s , state.last_changed.replace=%s",
|
||||
self,
|
||||
@@ -1648,7 +1676,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
for under in self._underlyings:
|
||||
await under.turn_off()
|
||||
|
||||
async def _async_manage_window_auto(self):
|
||||
async def _async_manage_window_auto(self, in_cycle=False):
|
||||
"""The management of the window auto feature"""
|
||||
|
||||
async def dearm_window_auto(_):
|
||||
@@ -1678,9 +1706,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if not self._window_auto_algo:
|
||||
return
|
||||
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
temperature=self._cur_temp, datetime_measure=self._last_temperature_mesure
|
||||
)
|
||||
if in_cycle:
|
||||
slope = self._window_auto_algo.check_age_last_measurement(
|
||||
temperature=self._ema_temp,
|
||||
datetime_now=datetime.now(get_tz(self._hass)),
|
||||
)
|
||||
else:
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
temperature=self._ema_temp,
|
||||
datetime_measure=self._last_temperature_mesure,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto is on, check the alert. last slope is %.3f",
|
||||
self,
|
||||
@@ -2029,6 +2065,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._attr_preset_mode,
|
||||
)
|
||||
|
||||
# check auto_window conditions
|
||||
await self._async_manage_window_auto(in_cycle=True)
|
||||
|
||||
# Issue 56 in over_climate mode, if the underlying climate is not initialized, try to initialize it
|
||||
for under in self._underlyings:
|
||||
if not under.is_initialized:
|
||||
@@ -2155,6 +2194,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"temperature_unit": self.temperature_unit,
|
||||
"is_device_active": self.is_device_active,
|
||||
"ema_temp": self._ema_temp,
|
||||
}
|
||||
|
||||
@callback
|
||||
|
||||
85
custom_components/versatile_thermostat/ema.py
Normal file
85
custom_components/versatile_thermostat/ema.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""The Estimated Mobile Average calculation used for temperature slope
|
||||
and maybe some others feature"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime, tzinfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_DECAY_SEC = 0
|
||||
# As for the EMA calculation of irregular time series, I've seen that it might be useful to
|
||||
# have an upper limit for alpha in case the last measurement was too long ago.
|
||||
# For example when using a half life of 10 minutes a measurement that is 60 minutes ago
|
||||
# (if there's nothing inbetween) would contribute to the smoothed value with 1,5%,
|
||||
# giving the current measurement 98,5% relevance. It could be wise to limit the alpha to e.g. 4x the half life (=0.9375).
|
||||
MAX_ALPHA = 0.5
|
||||
|
||||
|
||||
class ExponentialMovingAverage:
|
||||
"""A class that will do the Estimated Mobile Average calculation"""
|
||||
|
||||
def __init__(
|
||||
self, vterm_name: str, halflife: float, timezone: tzinfo, precision: int = 3
|
||||
):
|
||||
"""The halflife is the duration in secondes of a normal cycle"""
|
||||
self._halflife: float = halflife
|
||||
self._timezone = timezone
|
||||
self._current_ema: float = None
|
||||
self._last_timestamp: datetime = datetime.now(self._timezone)
|
||||
self._name = vterm_name
|
||||
self._precision = precision
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"EMA-{self._name}"
|
||||
|
||||
def calculate_ema(self, measurement: float, timestamp: datetime) -> float | None:
|
||||
"""Calculate the new EMA from a new measurement measured at timestamp
|
||||
Return the EMA or None if all parameters are not initialized now
|
||||
"""
|
||||
|
||||
if measurement is None or timestamp is None:
|
||||
_LOGGER.warning(
|
||||
"%s - Cannot calculate EMA: measurement and timestamp are mandatory. This message can be normal at startup but should not persist",
|
||||
self,
|
||||
)
|
||||
return measurement
|
||||
|
||||
if self._current_ema is None:
|
||||
_LOGGER.debug(
|
||||
"%s - First init of the EMA",
|
||||
self,
|
||||
)
|
||||
self._current_ema = measurement
|
||||
self._last_timestamp = timestamp
|
||||
return self._current_ema
|
||||
|
||||
time_decay = (timestamp - self._last_timestamp).total_seconds()
|
||||
if time_decay < MIN_TIME_DECAY_SEC:
|
||||
_LOGGER.debug(
|
||||
"%s - time_decay %s is too small (< %s). Forget the measurement",
|
||||
self,
|
||||
time_decay,
|
||||
MIN_TIME_DECAY_SEC,
|
||||
)
|
||||
return self._current_ema
|
||||
|
||||
alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife)
|
||||
# capping alpha to avoid gap if last measurement was long time ago
|
||||
alpha = min(alpha, MAX_ALPHA)
|
||||
new_ema = alpha * measurement + (1 - alpha) * self._current_ema
|
||||
|
||||
self._last_timestamp = timestamp
|
||||
self._current_ema = new_ema
|
||||
_LOGGER.debug(
|
||||
"%s - timestamp=%s alpha=%.2f measurement=%.2f current_ema=%.2f new_ema=%.2f",
|
||||
self,
|
||||
timestamp,
|
||||
alpha,
|
||||
measurement,
|
||||
self._current_ema,
|
||||
new_ema,
|
||||
)
|
||||
|
||||
return round(self._current_ema, self._precision)
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" This file implements the Open Window by temperature algorithm
|
||||
This algo works the following way:
|
||||
- each time a new temperature is measured
|
||||
@@ -12,8 +13,14 @@ from datetime import datetime
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# To filter bad values
|
||||
MIN_DELTA_T_SEC = 30 # two temp mesure should be > 10 sec
|
||||
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
|
||||
MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec
|
||||
MAX_SLOPE_VALUE = (
|
||||
120 # slope cannot be > 2°/min or < -2°/min -> else this is an aberrant point
|
||||
)
|
||||
|
||||
MAX_DURATION_MIN = 30 # a fake data point is added in the cycle if last measurement was older than 30 min
|
||||
|
||||
MIN_NB_POINT = 4 # do not calculate slope until we have enough point
|
||||
|
||||
|
||||
class WindowOpenDetectionAlgorithm:
|
||||
@@ -24,6 +31,7 @@ class WindowOpenDetectionAlgorithm:
|
||||
_last_slope: float
|
||||
_last_datetime: datetime
|
||||
_last_temperature: float
|
||||
_nb_point: int
|
||||
|
||||
def __init__(self, alert_threshold, end_alert_threshold) -> None:
|
||||
"""Initalize a new algorithm with the both threshold"""
|
||||
@@ -31,9 +39,24 @@ class WindowOpenDetectionAlgorithm:
|
||||
self._end_alert_threshold = end_alert_threshold
|
||||
self._last_slope = None
|
||||
self._last_datetime = None
|
||||
self._nb_point = 0
|
||||
|
||||
def check_age_last_measurement(self, temperature, datetime_now) -> float:
|
||||
""" " Check if last measurement is old and add
|
||||
a fake measurement point if this is the case
|
||||
"""
|
||||
if self._last_datetime is None:
|
||||
return self.add_temp_measurement(temperature, datetime_now)
|
||||
|
||||
delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) / 60.0
|
||||
if delta_t_sec >= MAX_DURATION_MIN:
|
||||
return self.add_temp_measurement(temperature, datetime_now, False)
|
||||
else:
|
||||
# do nothing
|
||||
return self._last_slope
|
||||
|
||||
def add_temp_measurement(
|
||||
self, temperature: float, datetime_measure: datetime
|
||||
self, temperature: float, datetime_measure: datetime, store_date: bool = True
|
||||
) -> float:
|
||||
"""Add a new temperature measurement
|
||||
returns the last slope
|
||||
@@ -42,6 +65,7 @@ class WindowOpenDetectionAlgorithm:
|
||||
_LOGGER.debug("First initialisation")
|
||||
self._last_datetime = datetime_measure
|
||||
self._last_temperature = temperature
|
||||
self._nb_point = self._nb_point + 1
|
||||
return None
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -61,8 +85,10 @@ class WindowOpenDetectionAlgorithm:
|
||||
)
|
||||
return lspe
|
||||
|
||||
delta_t_hour = delta_t / 60.0
|
||||
|
||||
delta_temp = float(temperature - self._last_temperature)
|
||||
new_slope = delta_temp / delta_t
|
||||
new_slope = delta_temp / delta_t_hour
|
||||
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
|
||||
_LOGGER.debug(
|
||||
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
|
||||
@@ -72,21 +98,28 @@ class WindowOpenDetectionAlgorithm:
|
||||
return lspe
|
||||
|
||||
if self._last_slope is None:
|
||||
self._last_slope = new_slope
|
||||
self._last_slope = round(new_slope, 2)
|
||||
else:
|
||||
self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
|
||||
self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 2)
|
||||
|
||||
# if we are in cycle check and so adding a fake datapoint, we don't store the event datetime
|
||||
# so that, when we will receive a real temperature point we will not calculate a wrong slope
|
||||
if store_date:
|
||||
self._last_datetime = datetime_measure
|
||||
|
||||
self._last_datetime = datetime_measure
|
||||
self._last_temperature = temperature
|
||||
|
||||
self._nb_point = self._nb_point + 1
|
||||
_LOGGER.debug(
|
||||
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f",
|
||||
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f nb_point=%s",
|
||||
delta_t,
|
||||
delta_temp,
|
||||
new_slope,
|
||||
lspe,
|
||||
self._last_slope,
|
||||
self._nb_point,
|
||||
)
|
||||
|
||||
return self._last_slope
|
||||
|
||||
def is_window_open_detected(self) -> bool:
|
||||
@@ -94,22 +127,20 @@ class WindowOpenDetectionAlgorithm:
|
||||
if self._alert_threshold is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._last_slope < -self._alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
|
||||
return False
|
||||
|
||||
return self._last_slope < -self._alert_threshold
|
||||
|
||||
def is_window_close_detected(self) -> bool:
|
||||
"""True if the last calculated slope is above (cause negative) the _end_alert_threshold"""
|
||||
if self._end_alert_threshold is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._last_slope >= self._end_alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
|
||||
return False
|
||||
|
||||
return self._last_slope >= self._end_alert_threshold
|
||||
|
||||
@property
|
||||
def last_slope(self) -> float:
|
||||
|
||||
@@ -49,7 +49,8 @@ class PITemperatureRegulator:
|
||||
self.target_temp = target_temp
|
||||
# Do not reset the accumulated error
|
||||
# Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now.
|
||||
self.accumulated_error = 0
|
||||
if self.accumulated_error < 0:
|
||||
self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(
|
||||
self, internal_temp: float, external_temp: float
|
||||
|
||||
@@ -51,6 +51,7 @@ async def async_setup_entry(
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
|
||||
EMATemperatureSensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
@@ -483,7 +484,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
return self.my_climate.temperature_unit + "/min"
|
||||
return self.my_climate.temperature_unit + "/hour"
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
@@ -505,17 +506,15 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
|
||||
self.my_climate.regulated_target_temp
|
||||
):
|
||||
raise ValueError(
|
||||
f"Sensor has illegal state {self.my_climate.regulated_target_temp}"
|
||||
)
|
||||
new_temp = self.my_climate.regulated_target_temp
|
||||
if new_temp is None:
|
||||
return
|
||||
|
||||
if math.isnan(new_temp) or math.isinf(new_temp):
|
||||
raise ValueError(f"Sensor has illegal state {new_temp}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.regulated_target_temp, self.suggested_display_precision
|
||||
)
|
||||
self._attr_native_value = round(new_temp, self.suggested_display_precision)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -542,3 +541,54 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 1
|
||||
|
||||
|
||||
class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a Exponential Moving Average temp"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the regulated temperature sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "EMA temperature"
|
||||
self._attr_unique_id = f"{self._device_name}_ema_temperature"
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
new_ema = self.my_climate.ema_temperature
|
||||
if new_ema is None:
|
||||
return
|
||||
|
||||
if math.isnan(new_ema) or math.isinf(new_ema):
|
||||
raise ValueError(f"Sensor has illegal state {new_ema}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = new_ema
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:thermometer-lines"
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
@@ -91,14 +91,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
}
|
||||
@@ -260,14 +260,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
}
|
||||
|
||||
@@ -91,14 +91,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used"
|
||||
}
|
||||
@@ -260,14 +260,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used"
|
||||
}
|
||||
|
||||
@@ -91,14 +91,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/min)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
||||
}
|
||||
@@ -261,14 +261,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_auto_open_threshold": "seuil haut de chute de température pour la détection automatique (en °/min)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
||||
}
|
||||
|
||||
@@ -87,14 +87,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Entity id sensore finestra",
|
||||
"window_delay": "Ritardo sensore finestra (secondi)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
|
||||
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
@@ -245,16 +245,16 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Entity id sensore finestra",
|
||||
"window_delay": "Ritardo sensore finestra (secondi)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
|
||||
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 minuti. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
|
||||
@@ -91,14 +91,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "ID entity snímača okna",
|
||||
"window_delay": "Oneskorenie snímača okna (sekundy)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/min)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/min)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 0,05 a 0,1. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
|
||||
}
|
||||
@@ -260,14 +260,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "ID entity snímača okna",
|
||||
"window_delay": "Oneskorenie snímača okna (sekundy)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/min)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/min)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 0,05 a 0,1. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
|
||||
}
|
||||
|
||||
@@ -363,12 +363,12 @@ async def test_over_climate_regulation_limitations(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 16, event_timestamp)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 12, event_timestamp)
|
||||
|
||||
# the regulated should have been done
|
||||
assert entity.regulated_target_temp != old_regulated_temp
|
||||
assert entity.regulated_target_temp >= entity.target_temperature
|
||||
assert (
|
||||
entity.regulated_target_temp == 17 + 0.5
|
||||
entity.regulated_target_temp == 17 + 1.5
|
||||
) # 0.7 without round_to_nearest
|
||||
|
||||
54
tests/test_ema.py
Normal file
54
tests/test_ema.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" Tests de EMA calculation"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from custom_components.versatile_thermostat.ema import ExponentialMovingAverage
|
||||
|
||||
from .commons import get_tz
|
||||
|
||||
|
||||
def test_ema_basics(hass: HomeAssistant):
|
||||
"""Test the EMA calculation with basic features"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
the_ema = ExponentialMovingAverage(
|
||||
"test",
|
||||
# 5 minutes
|
||||
300,
|
||||
# Needed for time calculation
|
||||
get_tz(hass),
|
||||
1,
|
||||
)
|
||||
|
||||
assert the_ema
|
||||
|
||||
current_timestamp = now
|
||||
# First initialization
|
||||
assert the_ema.calculate_ema(20, current_timestamp) == 20
|
||||
|
||||
current_timestamp = current_timestamp + timedelta(minutes=1)
|
||||
# One minute later, same temperature. EMA temperature should not have change
|
||||
assert the_ema.calculate_ema(20, current_timestamp) == 20
|
||||
|
||||
# Too short measurement should be ignored
|
||||
assert the_ema.calculate_ema(2000, current_timestamp) == 20
|
||||
|
||||
current_timestamp = current_timestamp + timedelta(seconds=4)
|
||||
assert the_ema.calculate_ema(20, current_timestamp) == 20
|
||||
|
||||
# a new normal measurement 5 minutes later
|
||||
current_timestamp = current_timestamp + timedelta(minutes=5)
|
||||
ema = the_ema.calculate_ema(25, current_timestamp)
|
||||
assert ema > 20
|
||||
assert ema == 22.5
|
||||
|
||||
# a big change in a short time does have a limited effect
|
||||
current_timestamp = current_timestamp + timedelta(seconds=5)
|
||||
ema = the_ema.calculate_ema(30, current_timestamp)
|
||||
assert ema > 22.5
|
||||
assert ema < 23
|
||||
assert ema == 22.6
|
||||
@@ -386,6 +386,7 @@ async def test_multiple_climates(
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
@@ -486,6 +487,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
""" Test the OpenWindow algorithm """
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from custom_components.versatile_thermostat.open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from custom_components.versatile_thermostat.open_window_algorithm import (
|
||||
WindowOpenDetectionAlgorithm,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -13,24 +15,34 @@ async def test_open_window_algo(
|
||||
):
|
||||
"""Tests the Algo"""
|
||||
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||
the_algo = WindowOpenDetectionAlgorithm(60.0, 0.0)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 2 measurement
|
||||
# We need at least 4 measurement
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
event_timestamp = now - timedelta(minutes=9)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=8)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
@@ -41,62 +53,62 @@ async def test_open_window_algo(
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=9, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -0.5
|
||||
assert the_algo.last_slope == -0.5
|
||||
assert last_slope == -48.0
|
||||
assert the_algo.last_slope == -48.0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 2 degre less in one minute (value will be rejected)
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == (-48.0 * 0.2 - 120.0 * 0.8)
|
||||
assert the_algo.last_slope == -105.6
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 1 degre less
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -105.6 * 0.2 - 60.0 * 0.8
|
||||
assert the_algo.last_slope == -69.12
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 0 degre less
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == round(-69.12 * 0.2 - 0.0 * 0.8, 2)
|
||||
assert the_algo.last_slope == -13.82
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 1 degre more
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -0.5 / 2.0 - 2.0 / 2.0
|
||||
assert the_algo.last_slope == -1.25
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 1 degre less
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.25 / 2 - 1.0 / 2.0
|
||||
assert the_algo.last_slope == -1.125
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 0 degre less
|
||||
event_timestamp = now - timedelta(minutes=0)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.125 / 2
|
||||
assert the_algo.last_slope == -1.125 / 2
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 1 degre more
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.125 / 4 + 0.5
|
||||
assert the_algo.last_slope == 0.21875
|
||||
assert last_slope == round(-13.82 * 0.2 + 60.0 * 0.8, 2)
|
||||
assert the_algo.last_slope == 45.24
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
@@ -106,7 +118,7 @@ async def test_open_window_algo_wrong(
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo with wrong date"""
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||
the_algo = WindowOpenDetectionAlgorithm(60.0, 0.0)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
@@ -134,3 +146,95 @@ async def test_open_window_algo_wrong(
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
|
||||
async def test_open_window_algo_fake_point(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo with adding fake point"""
|
||||
|
||||
the_algo = WindowOpenDetectionAlgorithm(3.0, 0.1)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
event_timestamp = now
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=10, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 4 measurement
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now + timedelta(minutes=3)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# No slope because same temperature
|
||||
assert last_slope == 0
|
||||
assert the_algo.last_slope == 0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now + timedelta(minutes=4)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=9, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -48.0
|
||||
assert the_algo.last_slope == -48.0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True # One degre in one minute
|
||||
|
||||
# 1 Add a fake point one minute later
|
||||
event_timestamp = now + timedelta(minutes=5)
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=8, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# The slope not have change (fake point is ignored)
|
||||
assert last_slope == -48.0
|
||||
assert the_algo.last_slope == -48.0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True # One degre in one minute
|
||||
|
||||
# 2 Add a fake point 31 minute later -> +2 degres in 32 minutes
|
||||
event_timestamp = event_timestamp + timedelta(minutes=31)
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=10, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# The slope should have change (fake point is added)
|
||||
assert last_slope == -8.1
|
||||
assert the_algo.last_slope == -8.1
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# 3 Add a 2nd fake point 30 minute later -> +3 degres in 30 minutes
|
||||
event_timestamp = event_timestamp + timedelta(minutes=31)
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=13, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# The slope should have change (fake point is added)
|
||||
assert last_slope == 0.67
|
||||
assert the_algo.last_slope == 0.67
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
@@ -296,6 +296,14 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -307,13 +315,13 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity.is_device_active is True
|
||||
assert entity.last_temperature_slope == 0.0
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
@@ -329,14 +337,14 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 2
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
@@ -347,7 +355,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": -1.0},
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": -6.24},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
@@ -365,14 +373,14 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 17.9, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == -0.1 * 0.5 - 1 * 0.5
|
||||
assert round(entity.last_temperature_slope, 3) == -7.49
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
@@ -390,7 +398,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
@@ -405,7 +413,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
{
|
||||
"type": "end",
|
||||
"cause": "end of slope alert",
|
||||
"curve_slope": 0.27500000000000036,
|
||||
"curve_slope": 0.42,
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -413,7 +421,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
)
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == 0.275
|
||||
assert entity.last_temperature_slope == 0.42
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is True
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
@@ -451,8 +459,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
@@ -477,6 +485,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -486,12 +502,13 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
# This is the 3rd measurment. Slope is not ready
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The climate turns on but was alredy on
|
||||
assert mock_set_hvac_mode.call_count == 0
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity.last_temperature_slope == 0.0
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
@@ -505,9 +522,13 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
|
||||
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
# The heater turns off
|
||||
mock_send_event.assert_has_calls(
|
||||
@@ -518,20 +539,22 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
{
|
||||
"type": "start",
|
||||
"cause": "slope alert",
|
||||
"curve_slope": -1.0,
|
||||
"curve_slope": -6.24,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_set_hvac_mode.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
|
||||
# Waits for automatic disable
|
||||
# This is to avoid that the slope stayx under 6, else we will reactivate the window immediatly
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp, sleep=False)
|
||||
assert entity.last_temperature_slope > -6.0
|
||||
|
||||
# Waits for automatic disable
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -542,14 +565,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
):
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert mock_set_hvac_mode.call_count == 1
|
||||
assert round(entity.last_temperature_slope, 3) == -1
|
||||
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
|
||||
assert mock_set_hvac_mode.call_count == 1
|
||||
assert round(entity.last_temperature_slope, 3) == -0.29
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
@@ -576,7 +599,7 @@ async def test_window_auto_no_on_percent(
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
"boost_temp": 20,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
@@ -588,8 +611,8 @@ async def test_window_auto_no_on_percent(
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
@@ -610,10 +633,18 @@ async def test_window_auto_no_on_percent(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 21
|
||||
assert entity.target_temperature == 20
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 21, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 21, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 21, event_timestamp)
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -625,12 +656,12 @@ async def test_window_auto_no_on_percent(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 21.5, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 21, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
# The heater don't turns on
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity.last_temperature_slope == 0.0
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
@@ -647,16 +678,19 @@ async def test_window_auto_no_on_percent(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
||||
|
||||
# The heater turns on but no alert because the heater was not heating
|
||||
assert entity.proportional_algorithm.on_percent == 0.0
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == -1.5
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 1
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
# The algo calculate open ...
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
# But the entity is still on
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
@@ -831,8 +865,8 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
@@ -857,6 +891,14 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -868,12 +910,12 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity.is_device_active is True
|
||||
assert entity.last_temperature_slope == 0.0
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
@@ -881,7 +923,6 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
# send one degre down in one minute with window bypass on
|
||||
await entity.service_set_window_bypass_state(True)
|
||||
assert entity.window_bypass_state is True
|
||||
# entity._window_bypass_state = True
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -893,7 +934,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
|
||||
|
||||
# No change should have been done
|
||||
@@ -901,7 +942,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
|
||||
Reference in New Issue
Block a user