From 31a2198c567fcf0a4d2873c0b4bd8b6583f2cb69 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Fri, 1 Dec 2023 19:16:56 +0000 Subject: [PATCH] =?UTF-8?q?Change=20auto=20window=20threshold=20in=20?= =?UTF-8?q?=C2=B0/hour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../open_window_algorithm.py | 12 +- .../versatile_thermostat/sensor.py | 2 +- .../versatile_thermostat/strings.json | 12 +- .../versatile_thermostat/translations/en.json | 12 +- .../versatile_thermostat/translations/fr.json | 12 +- .../versatile_thermostat/translations/it.json | 16 +-- .../versatile_thermostat/translations/sk.json | 12 +- tests/test_open_window_algo.py | 118 +++++++++++++-- tests/test_window.py | 136 +++++++++++------- 9 files changed, 230 insertions(+), 102 deletions(-) diff --git a/custom_components/versatile_thermostat/open_window_algorithm.py b/custom_components/versatile_thermostat/open_window_algorithm.py index a1eda39a..2e83aa67 100644 --- a/custom_components/versatile_thermostat/open_window_algorithm.py +++ b/custom_components/versatile_thermostat/open_window_algorithm.py @@ -14,7 +14,9 @@ # To filter bad values MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec -MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point +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 @@ -83,8 +85,10 @@ def add_temp_measurement( ) 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", @@ -94,9 +98,9 @@ def add_temp_measurement( return lspe if self._last_slope is None: - self._last_slope = round(new_slope, 4) + self._last_slope = round(new_slope, 2) else: - self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 4) + 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 diff --git a/custom_components/versatile_thermostat/sensor.py b/custom_components/versatile_thermostat/sensor.py index 78fd8d8a..f11d7348 100644 --- a/custom_components/versatile_thermostat/sensor.py +++ b/custom_components/versatile_thermostat/sensor.py @@ -484,7 +484,7 @@ def native_unit_of_measurement(self) -> str | None: 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: diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index d41eb0b0..2e0391f5 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -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" } diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 4b5fa0b1..633260ab 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -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" } diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index e7a4477d..777f3f3c 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -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" } diff --git a/custom_components/versatile_thermostat/translations/it.json b/custom_components/versatile_thermostat/translations/it.json index acbd7c85..43c9a66e 100644 --- a/custom_components/versatile_thermostat/translations/it.json +++ b/custom_components/versatile_thermostat/translations/it.json @@ -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": { diff --git a/custom_components/versatile_thermostat/translations/sk.json b/custom_components/versatile_thermostat/translations/sk.json index 818d34c3..3dc7415b 100644 --- a/custom_components/versatile_thermostat/translations/sk.json +++ b/custom_components/versatile_thermostat/translations/sk.json @@ -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" } diff --git a/tests/test_open_window_algo.py b/tests/test_open_window_algo.py index b382087e..48a85e00 100644 --- a/tests/test_open_window_algo.py +++ b/tests/test_open_window_algo.py @@ -15,7 +15,7 @@ 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 @@ -59,8 +59,8 @@ async def test_open_window_algo( ) # A slope is calculated - assert last_slope == -0.8 - assert the_algo.last_slope == -0.8 + 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 @@ -71,8 +71,8 @@ async def test_open_window_algo( ) # A slope is calculated - assert last_slope == -0.5 / 2.0 - 2.0 / 2.0 - assert the_algo.last_slope == -1.25 + 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 @@ -83,8 +83,8 @@ async def test_open_window_algo( ) # A slope is calculated - assert last_slope == -1.25 / 2 - 1.0 / 2.0 - assert the_algo.last_slope == -1.125 + 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 @@ -95,20 +95,20 @@ async def test_open_window_algo( ) # A slope is calculated - assert last_slope == -1.125 / 2 - assert the_algo.last_slope == -1.125 / 2 + 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) + 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 == -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 @@ -118,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 @@ -146,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 diff --git a/tests/test_window.py b/tests/test_window.py index 82e4ab1c..4bb6477e 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -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 }, ) @@ -478,12 +486,12 @@ 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=9) - # await send_temperature_change_event(entity, 19, event_timestamp) - # event_timestamp = now - timedelta(minutes=8) - # await send_temperature_change_event(entity, 19, event_timestamp) - # event_timestamp = now - timedelta(minutes=7) - # await send_temperature_change_event(entity, 19, event_timestamp) + 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( @@ -495,12 +503,12 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st return_value=True, ): # This is the 3rd measurment. Slope is not ready - event_timestamp = now - timedelta(minutes=4) + 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 @@ -514,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( @@ -527,19 +539,21 @@ 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 + # 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" @@ -551,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() @@ -585,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, @@ -597,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 }, ) @@ -619,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" @@ -634,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 @@ -656,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 @@ -840,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 }, ) @@ -866,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" @@ -877,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 @@ -890,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" @@ -902,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 @@ -910,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