Compare commits

..

14 Commits

Author SHA1 Message Date
Jean-Marc Collin
ba69319198 Issue #619 - manual hvac_off should be prioritized over window and auto-start/stop hvac_off (#622)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-10 10:17:53 +01:00
Jean-Marc Collin
f9df925181 Issue #615 - VTherm switch to manual on its own (#618)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-09 18:44:13 +01:00
Jean-Marc Collin
2d72efe447 Issue 600 energy can be negative after configuration (#614)
* Add logs to diagnose the case

* Issue #552 (#608)

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>

* Fix typo (#607)

* - Force writing state when entity is removed
- Fix bug with issue #552 on CONF_USE_CENTRAL_BOILER_FEATURE which should be proposed on a central configuration
- Improve reload of entity to avoid reloading all VTherm. Only the reconfigured one will be reloaded

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2024-11-07 21:57:08 +01:00
Ludovic BOUÉ
95af6eba97 Fix typo (#607) 2024-11-05 22:47:42 +01:00
Jean-Marc Collin
06dc537767 Issue #552 (#608)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-05 22:39:26 +01:00
Joeri Colman
2d79d961dc Update en.json (#604)
fixed typo
2024-11-05 10:40:49 +01:00
Jean-Marc Collin
027bf8386b Add message into issue template. 2024-11-05 06:37:27 +00:00
Jean-Marc Collin
a0e548ef71 Release 2024-11-03 22:16:15 +00:00
Jean-Marc Collin
132519b471 Merge #590 and fix some tests 2024-11-03 22:12:56 +00:00
hilburn
e6c330fc9d Underlying config changes (#590)
* Changes config_flow to allow dynamic length list of underlying entities
Updates previously defined 4x entries to new config style
Changes to thermostat_X to load underlying entities from list
Changes to thermostat X to display underlying entities as a list - COULD BREAK EXISTING TEMPLATES

* Modifies tests to use the new list format

* Added English translation for UI

* Removed all references to individual entities in strings/en.json

* Fix merge mistake

---------

Co-authored-by: Jean-Marc Collin <jm.collin.78@gmail.com>
2024-11-03 22:52:19 +01:00
Jean-Marc Collin
968e8286ea Add some infos 2024-11-03 21:50:42 +00:00
hilburn
0f60c070ab Preset display tweaks (#599)
* Addded Frost Preset to translations
Added Icons for Shedding, Safety, Manual and Frost Presets

* Fixed French Translation
2024-11-03 11:50:37 +01:00
Jean-Marc Collin
810430f7b1 Update README.md
#597
2024-11-02 19:16:24 +01:00
Jean-Marc Collin
b4860c2b8d Issue 585 add auto start/stop feature (#594)
* Migrate to HA 2024.10.4

* Auto start/stop alog and testu + ConfigFlow

* With config flow ok

* Change algo

* All is fine

* Add change_preset test

* + comment

* FIX too much start/stop

* Change algo to take slop into account

* Allow calculation even if slope is None

* With enable + tests + hysteresis in calculation

* Add hvac_off_reason and test with window interaction

* Fix some tests

* Restore saved_state

* Release

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-01 18:54:22 +01:00
28 changed files with 2144 additions and 845 deletions

View File

@@ -152,6 +152,7 @@ climate:
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3

View File

@@ -4,6 +4,8 @@ about: Create a report to help us improve
---
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.

View File

@@ -31,6 +31,7 @@
- [Compensation de la température interne](#compensation-de-la-température-interne)
- [Synthèse de l'algorithme d'auto-régulation](#synthèse-de-lalgorithme-dauto-régulation)
- [Le mode auto-fan](#le-mode-auto-fan)
- [Le démarrage / arrêt automatique](#le-démarrage--arrêt-automatique)
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
- [Configurer les températures préréglées](#configurer-les-températures-préréglées)
@@ -92,6 +93,9 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> ![Nouveau](images/new-icon.png) _*Historique des dernières versions*_
> * **Release 6.5** :
> - Ajout d'une nouvelle fonction permettant l'arrêt et la relance automatique d'un VTherm `over_climate` [585](https://github.com/jmcollin78/versatile_thermostat/issues/585)
> - Amélioration de la gestion des ouvertures au démarrage. Permet de mémoriser et de recalculer l'état d'une ouverture au redémarage de Home Assistant [504](https://github.com/jmcollin78/versatile_thermostat/issues/504)
> * **Release 6.0** :
> - Ajout d'entités du domaine Number permettant de configurer les températures des presets [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> - Refonte complète du menu de configuration pour supprimer les températures et utililsation d'un menu au lieu d'un tunnel de configuration [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
@@ -100,14 +104,14 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> - ajout de seuils de régulation pour les `over_valve` pour éviter de trop vider la batterie des TRV [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
> - ajout d'une option permettant d'utiliser la température interne d'un TRV pour forcer l' auto-régulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
> - ajout d'une fonction de keep-alive pour les VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
<details>
<summary>Autres versions</summary>
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
> * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
@@ -510,6 +514,17 @@ Il faut évidemment que votre équipement sous-jacent soit équipée d'une venti
Si votre équipement ne comprend pas le mode Turbo, le mode Forte` sera utilisé en remplacement.
Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie.
#### Le démarrage / arrêt automatique
Cette fonction a été introduite en 6.5.0. Elle permet d'autoriser VTherm a stopper un équipement qui n'a pas besoin d'être allumé et de le redémarrer lorsque les conditions le réclame. Cette fonction est munie de 3 réglages qui permettent d'arrêter / relancer plus ou moins rapidement l'équipement.
Pour l'utiliser, vous devez :
1. Ajouter la fonction `Avec démmarrage et extinction automatique` dans le menu 'Fonctions',
2. Paramétrer le niveau de détection dans l'option 'Allumage/extinction automatique' qui s'affiche lorsque la fonction a été activée. Vous choisissez le niveau de détection entre 'Lent', 'Moyen' et 'Rapide'. Les arrêts/relances seront plus nombreux avec le niveau 'Rapide'.
Une fois paramétré, vous aurez maintenant une nouvelle entité de type `switch` qui vous permet d'autoriser ou non l'arrêt/relance automatique sans toucher à la configuration. Cette entité est disponible sur l'appareil VTherm et se nomme `switch.<name>_enable_auto_start_stop`. Cochez la pour autoriser le démarrage et extinction automatique.
L'algorithme de détection est décrit [ici](https://github.com/jmcollin78/versatile_thermostat/issues/585).
### Pour un thermostat de type ```thermostat_over_valve```:
![image](images/config-linked-entity3.png)
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
@@ -911,6 +926,8 @@ context:
| ``central_boiler_activation_service`` | Service d'activation de la chaudière | - | - | - | X |
| ``central_boiler_deactivation_service`` | Service de desactivation de la chaudière | - | - | - | X |
| ``used_by_controls_central_boiler`` | Indique si le VTherm contrôle la chaudière centrale | X | X | X | - |
| ``use_auto_start_stop_feature`` | Indique si la fonction de démarrage/extinction automatique est activée | - | X | - | - |
| ``auto_start_stop_lvel`` | Le niveau de détection de l'auto start/stop | - | X | - | - |
</details>
# Exemples de réglage
@@ -1168,6 +1185,9 @@ Les attributs personnalisés sont les suivants :
| ``is_controlled_by_central_mode`` | True si le VTherm peut être controlé de façon centrale |
| ``last_central_mode`` | Le dernier mode central utilisé (None si le VTherm n'est pas controlé en central) |
| ``is_used_by_central_boiler`` | Indique si le VTherm peut contrôler la chaudière centrale |
| ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter |
| ``auto_start_stop_level`` | Indique le niveau d'auto start/stop |
| ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel |
# Quelques résultats

View File

@@ -32,6 +32,7 @@
- [Internal temperature compensation](#internal-temperature-compensation)
- [synthesis of the self-regulation algorithm](#synthesis-of-the-self-regulation-algorithm)
- [Auto-fan mode](#auto-fan-mode)
- [Automatic start/stop](#automatic-startstop)
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
- [Configure the preset temperature](#configure-the-preset-temperature)
@@ -93,6 +94,9 @@
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
>![New](images/new-icon.png) _*Latest releases*_
> * **Release 6.5** :
> - Added a new function allowing the automatic shutdown and restart of a VTherm `over_climate` [585](https://github.com/jmcollin78/versatile_thermostat/issues/585)
> - Improved management of openings at startup. Allows to memorize and recalculate the state of an opening when restarting Home Assistant [504](https://github.com/jmcollin78/versatile_thermostat/issues/504)
> * **Release 6.0**:
> - Added entities from the Number domain to configure preset temperatures [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> - Complete redesign of the configuration menu to remove temperatures and use a menu instead of a configuration tunnel [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
@@ -101,13 +105,13 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
> - addition of regulation thresholds for the `over_valve` to avoid draining the TRV battery too much [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
> - added an option allowing the internal temperature of a TRV to be used to force self-regulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
> - added a keep-alive function for VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
<details>
<summary>Others releases</summary>
> * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
> * **Release 5.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
> * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
<details>
<summary>Others releases</summary>
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
@@ -500,6 +504,17 @@ Obviously your underlying equipment must be equipped with ventilation and be con
If your equipment does not include Turbo mode, Forte` mode will be used as a replacement.
Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen.
#### Automatic start/stop
This function was introduced in 6.5.0. It allows VTherm to stop equipment that does not need to be turned on and to restart it when conditions require it. This function has 3 settings that allow the equipment to be stopped/restarted more or less quickly.
To use it, you must:
1. Add the `Use the auto start and stop feature` function in the 'Features' menu,
2. Set the detection level in the `Auto start and stop` option that is displayed when the function has been activated. You choose the detection level between 'Slow', 'Medium' and 'Fast'. The 'Fast' level will result in more shutdowns/restarts.
Once configured, you will now have a new entity of type `switch` that allows you to authorize or not the automatic shutdown/restart without touching the configuration. This entity is available on the VTherm device and is called `switch.<name>_enable_auto_start_stop`. Check it to authorize the automatic startup and shutdown.
The detection algorithm is described [here](https://github.com/jmcollin78/versatile_thermostat/issues/585).
### For a thermostat of type ```thermostat_over_valve```:
![image](images/config-linked-entity3.png)
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
@@ -900,6 +915,8 @@ context:
| ``central_boiler_activation_service`` | Activation service of the boiler | - | - | - | X |
| ``central_boiler_deactivation_service`` | Deactivaiton service of the boiler | - | - | - | X |
| ``used_by_controls_central_boiler`` | Indicate if the VTherm control the central boiler | X | X | X | - |
| ``use_auto_start_stop_feature`` | Indique si la fonction de démarrage/extinction automatique est activée | - | X | - | - |
| ``auto_start_stop_lvel`` | Le niveau de détection de l'auto start/stop | - | X | - | - |
</details>
# Tuning examples
@@ -1155,6 +1172,9 @@ Custom attributes are the following:
| ``is_controlled_by_central_mode`` | True if the VTherm can be centrally controlled |
| ``last_central_mode`` | The last central mode used (None if the VTherm is not centrally controlled) |
| ``is_used_by_central_boiler`` | Indicate if the VTherm can control the central boiler |
| ``auto_start_stop_enable`` | Indicate if the VTherm is allowed to do auto start and stop |
| ``auto_start_stop_level`` | Give the level of auto start/stop |
| ``hvac_off_reason`` | Give the reason of stop of the VTherm. This could be Window, Auto-start/stop or Manual |
# Some results
@@ -1333,7 +1353,7 @@ Example of graph obtained with Plotly :
## And always better and better with the NOTIFIER daemon app to notify events
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
This is a great example of using the notifications described here [notification](#notifications).

View File

@@ -38,6 +38,22 @@ from .const import (
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_POWER_SENSOR,
CONF_PRESENCE_SENSOR,
CONF_UNDERLYING_LIST,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
)
from .vtherm_api import VersatileThermostatAPI
@@ -162,13 +178,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if hass.state == CoreState.running:
await api.reload_central_boiler_entities_list()
await api.init_vtherm_links()
await api.init_vtherm_links(entry.entry_id)
return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
_LOGGER.debug(
"Calling update_listener entry: entry_id='%s', value='%s'",
entry.entry_id,
entry.data,
)
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
await reload_all_vtherm(hass)
else:
@@ -177,7 +200,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api is not None:
await api.reload_central_boiler_entities_list()
await api.init_vtherm_links()
await api.init_vtherm_links(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -208,10 +231,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
)
new = {**config_entry.data}
if (
config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE)
if thermostat_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
new[CONF_USE_WINDOW_FEATURE] = True
new[CONF_USE_MOTION_FEATURE] = True
new[CONF_USE_POWER_FEATURE] = new.get(CONF_POWER_SENSOR, None) is not None
@@ -223,6 +245,50 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"add_central_boiler_control", False
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
if config_entry.data.get(CONF_UNDERLYING_LIST, None) is None:
underlying_list = []
if thermostat_type == CONF_THERMOSTAT_SWITCH:
underlying_list = [
config_entry.data.get(CONF_HEATER, None),
config_entry.data.get(CONF_HEATER_2, None),
config_entry.data.get(CONF_HEATER_3, None),
config_entry.data.get(CONF_HEATER_4, None),
]
elif thermostat_type == CONF_THERMOSTAT_CLIMATE:
underlying_list = [
config_entry.data.get(CONF_CLIMATE, None),
config_entry.data.get(CONF_CLIMATE_2, None),
config_entry.data.get(CONF_CLIMATE_3, None),
config_entry.data.get(CONF_CLIMATE_4, None),
]
elif thermostat_type == CONF_THERMOSTAT_VALVE:
underlying_list = [
config_entry.data.get(CONF_VALVE, None),
config_entry.data.get(CONF_VALVE_2, None),
config_entry.data.get(CONF_VALVE_3, None),
config_entry.data.get(CONF_VALVE_4, None),
]
new[CONF_UNDERLYING_LIST] = [
entity for entity in underlying_list if entity is not None
]
for key in [
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
]:
new.pop(key, None)
hass.config_entries.async_update_entry(
config_entry,
data=new,

View File

@@ -3,7 +3,7 @@
"""
import logging
from datetime import datetime, timedelta
from datetime import datetime
from typing import Literal
from homeassistant.components.climate import HVACMode
@@ -13,7 +13,6 @@ from .const import (
AUTO_START_STOP_LEVEL_FAST,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_SLOW,
CONF_AUTO_START_STOP_LEVELS,
TYPE_AUTO_START_STOP_LEVELS,
)
@@ -31,6 +30,9 @@ DT_MIN = {
# the measurement cycle (2 min)
CYCLE_SEC = 120
# A temp hysteresis to avoid rapid OFF/ON
TEMP_HYSTERESIS = 0.5
ERROR_THRESHOLD = {
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
@@ -79,7 +81,7 @@ class AutoStartStopDetectionAlgorithm:
saved_hvac_mode: HVACMode | None,
target_temp: float,
current_temp: float,
slope_min: float,
slope_min: float | None,
now: datetime,
) -> AUTO_START_STOP_ACTIONS:
"""Calculate an eventual action to do depending of the value in parameter"""
@@ -101,12 +103,7 @@ class AutoStartStopDetectionAlgorithm:
now,
)
if (
hvac_mode is None
or target_temp is None
or current_temp is None
or slope_min is None
):
if hvac_mode is None or target_temp is None or current_temp is None:
_LOGGER.debug(
"%s - No all mandatory parameters are set. Disable auto-start/stop",
self,
@@ -119,8 +116,8 @@ class AutoStartStopDetectionAlgorithm:
# reduce the error considering the dt between the last measurement
if self._last_calculation_date is not None:
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
# ignore two calls too near (< 1 min)
if dtmin <= 0.5:
# ignore two calls too near (< 24 sec)
if dtmin <= 0.2:
_LOGGER.debug(
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
self,
@@ -144,10 +141,15 @@ class AutoStartStopDetectionAlgorithm:
self._last_calculation_date = now
temp_at_dt = current_temp + slope_min * self._dt
# Check to turn-off
# When we hit the threshold, that mean we can turn off
if hvac_mode == HVACMode.HEAT:
if self._accumulated_error <= -self._error_threshold and slope_min >= 0:
if (
self._accumulated_error <= -self._error_threshold
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
):
_LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.",
self,
@@ -158,7 +160,10 @@ class AutoStartStopDetectionAlgorithm:
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.COOL:
if self._accumulated_error >= self._error_threshold and slope_min <= 0:
if (
self._accumulated_error >= self._error_threshold
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
):
_LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.",
self,
@@ -173,7 +178,7 @@ class AutoStartStopDetectionAlgorithm:
# check to turn on
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
if current_temp + slope_min * self._dt <= target_temp:
if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
_LOGGER.info(
"%s - We need to start, because it will be time to heat",
self,
@@ -187,7 +192,7 @@ class AutoStartStopDetectionAlgorithm:
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
if current_temp + slope_min * self._dt >= target_temp:
if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
_LOGGER.info(
"%s - We need to start, because it will be time to cool",
self,

View File

@@ -19,7 +19,10 @@ from homeassistant.core import (
)
from homeassistant.components.climate import ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.restore_state import (
RestoreEntity,
async_get as restore_async_get,
)
from homeassistant.helpers.entity import Entity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
@@ -62,72 +65,7 @@ from homeassistant.const import (
STATE_NOT_HOME,
)
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_POWER_SENSOR,
CONF_TEMP_SENSOR,
CONF_LAST_SEEN_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
CONF_PRESETS,
# CONF_PRESETS_AWAY,
# CONF_PRESETS_WITH_AC,
# CONF_PRESETS_AWAY_WITH_AC,
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT,
CONF_PRESENCE_SENSOR,
CONF_PRESET_POWER,
SUPPORT_FLAGS,
PRESET_FROST_PROTECTION,
PRESET_POWER,
PRESET_SECURITY,
PROPORTIONAL_FUNCTION_TPI,
PRESET_AWAY_SUFFIX,
CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
DEFAULT_SECURITY_MIN_ON_PERCENT,
DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_USE_MAIN_CENTRAL_CONFIG,
CONF_USE_TPI_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_WINDOW_CENTRAL_CONFIG,
CONF_USE_MOTION_CENTRAL_CONFIG,
CONF_USE_POWER_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_PRESENCE_FEATURE,
CONF_TEMP_MAX,
CONF_TEMP_MIN,
HIDDEN_PRESETS,
CONF_AC_MODE,
EventType,
ATTR_MEAN_POWER_CYCLE,
ATTR_TOTAL_ENERGY,
PRESET_AC_SUFFIX,
DEFAULT_SHORT_EMA_PARAMS,
CENTRAL_MODE_AUTO,
CENTRAL_MODE_STOPPED,
CENTRAL_MODE_HEAT_ONLY,
CENTRAL_MODE_COOL_ONLY,
CENTRAL_MODE_FROST_PROTECTION,
send_vtherm_event,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -149,6 +87,10 @@ def get_tz(hass: HomeAssistant):
return dt_util.get_time_zone(hass.config.time_zone)
_LOGGER_ENERGY = logging.getLogger(
"custom_components.versatile_thermostat.energy_debug"
)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device."""
@@ -199,6 +141,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"is_device_active",
"target_temperature_step",
"is_used_by_central_boiler",
"temperature_slope"
}
)
)
@@ -262,6 +205,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_translation_key = "versatile_thermostat"
self._total_energy = None
_LOGGER_ENERGY.debug("%s - _init_ resetting energy to None", self)
# because energy of climate is calculated in the thermostat we have to keep that here and not in underlying entity
self._underlying_climate_start_hvac_action_date = None
@@ -303,6 +247,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._use_central_config_temperature = False
self._hvac_off_reason: HVAC_OFF_REASONS | None = None
self.post_init(entry_infos)
def clean_central_config_doublon(
@@ -532,6 +478,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._presence_state = None
self._total_energy = None
_LOGGER_ENERGY.debug("%s - post_init_ resetting energy to None", self)
# Read the parameter from configuration.yaml if it exists
short_ema_params = DEFAULT_SHORT_EMA_PARAMS
@@ -647,14 +594,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# issue 428. Link to others entities will start at link
# await self.async_startup()
async def async_will_remove_from_hass(self):
"""Try to force backup of entity"""
_LOGGER_ENERGY.debug(
"%s - force write before remove. Energy is %s", self, self.total_energy
)
# Force dump in background
await restore_async_get(self.hass).async_dump_states()
def remove_thermostat(self):
"""Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self)
for under in self._underlyings:
under.remove_entity()
async def async_startup(self, central_configuration):
"""Triggered on startup, used to get old state and set internal states accordingly"""
"""Triggered on startup, used to get old state and set internal states accordingly. This is triggered by
VTherm API"""
_LOGGER.debug("%s - Calling async_startup", self)
_LOGGER.debug("%s - Calling async_startup_internal", self)
@@ -848,18 +805,29 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
else:
self._attr_preset_mode = PRESET_NONE
# Restore old hvac_off_reason
self._hvac_off_reason = old_state.attributes.get(HVAC_OFF_REASON_NAME, None)
if old_state.state in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
]:
self._hvac_mode = old_state.state
else:
if not self._hvac_mode:
self._hvac_mode = HVACMode.OFF
# restpre also saved info so that window detection will work
self._saved_hvac_mode = old_state.attributes.get("saved_hvac_mode", None)
self._saved_preset_mode = old_state.attributes.get(
"saved_preset_mode", None
)
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
self._total_energy = old_total_energy if old_total_energy else 0
self._total_energy = old_total_energy if old_total_energy is not None else 0
_LOGGER_ENERGY.debug(
"%s - get_my_previous_state restored energy is %s",
self,
self._total_energy,
)
self.restore_specific_previous_state(old_state)
else:
@@ -873,13 +841,20 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"No previously saved temperature, setting to %s", self._target_temp
)
self._total_energy = 0
_LOGGER_ENERGY.debug(
"%s - get_my_previous_state no previous state energy is %s",
self,
self._total_energy,
)
self._saved_target_temp = self._target_temp
# Set default state to off
if not self._hvac_mode:
self._hvac_mode = HVACMode.OFF
if not self.is_on and self.hvac_off_reason is None:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
self._saved_target_temp = self._target_temp
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@@ -987,16 +962,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property
def hvac_mode(self) -> HVACMode | None:
"""Return current operation."""
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
# delta will be managed by climate_state_change event.
# if self.is_over_climate:
# if one not OFF -> return it
# else OFF
# for under in self._underlyings:
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
# return mode
# return HVACMode.OFF
return self._hvac_mode
@property
@@ -1193,6 +1158,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""True if this VTHerm uses the central configuration temperature"""
return self._use_central_config_temperature
@property
def hvac_off_reason(self) -> HVAC_OFF_REASONS:
"""Returns the reason of the last switch to HVAC_OFF
This is useful for features that turns off the VTherm like
window detection or auto-start-stop"""
return self._hvac_off_reason
def underlying_entity_id(self, index=0) -> str | None:
"""The climate_entity_id. Added for retrocompatibility reason"""
if index < self.nb_underlying_entities:
@@ -1234,6 +1206,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if hvac_mode is None:
return
def save_state():
self.reset_last_change_time()
self.update_custom_attributes()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
# If we already are in OFF, the manual OFF should just overwrite the reason and saved_hvac_mode
if self._hvac_mode == HVACMode.OFF and hvac_mode == HVACMode.OFF:
_LOGGER.info(
"%s - already in OFF. Change the reason to MANUAL and erase the saved_havc_mode"
)
self._hvac_off_reason = HVAC_OFF_REASON_MANUAL
self._saved_hvac_mode = HVACMode.OFF
save_state()
return
self._hvac_mode = hvac_mode
# Delegate to all underlying
@@ -1256,11 +1246,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Ensure we update the current operation after changing the mode
self.reset_last_temperature_time()
self.reset_last_change_time()
if self._hvac_mode != HVACMode.OFF:
self.set_hvac_off_reason(None)
self.update_custom_attributes()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
save_state()
@overrides
async def async_set_preset_mode(
@@ -1760,6 +1749,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
for under in self._underlyings:
await under.check_initial_state(self._hvac_mode)
# Prevent from starting a VTherm if window is open
if (
self.is_window_auto_enabled
and self._window_sensor_entity_id is not None
and self._hass.states.is_state(self._window_sensor_entity_id, STATE_ON)
and self.is_on
and self.window_action == CONF_WINDOW_TURN_OFF
):
_LOGGER.info("%s - the window is open. Prevent starting the VTherm")
self._window_auto_state = True
self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF)
# Starts the initial control loop (don't wait for an update of temperature)
await self.async_control_heating(force=True)
@@ -2096,6 +2098,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_mode,
)
def set_hvac_off_reason(self, hvac_off_reason: HVAC_OFF_REASONS):
"""Set the reason of hvac_off"""
self._hvac_off_reason = hvac_off_reason
async def restore_hvac_mode(self, need_control_heating=False):
"""Restore a previous hvac_mod"""
await self.async_set_hvac_mode(self._saved_hvac_mode, need_control_heating)
@@ -2227,27 +2233,34 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if self.window_state is not STATE_ON and not first_init:
await self.restore_hvac_mode()
await self.restore_preset_mode()
elif self.window_state is STATE_ON and self.hvac_mode == HVACMode.OFF:
# do not restore but mark the reason of off with window detection
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
return
if old_central_mode == CENTRAL_MODE_AUTO and self.window_state is not STATE_ON:
save_all()
if new_central_mode == CENTRAL_MODE_STOPPED:
await self.async_set_hvac_mode(HVACMode.OFF)
if self.hvac_mode != HVACMode.OFF:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
return
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
if HVACMode.COOL in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.COOL)
else:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
return
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
if HVACMode.HEAT in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.HEAT)
else:
# if not already off
elif self.hvac_mode != HVACMode.OFF:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
return
@@ -2261,6 +2274,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
)
else:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
return
@@ -2440,17 +2454,27 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Change the window detection state.
new_state is on if an open window have been detected or off else
"""
if not new_state:
if new_state is False:
_LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s' if central_mode is not STOPPED",
"%s - Window is closed. Restoring hvac_mode '%s' if stopped by window detection or temperature %s",
self,
self._saved_hvac_mode,
self._saved_target_temp,
)
if self._window_action in [CONF_WINDOW_FROST_TEMP, CONF_WINDOW_ECO_TEMP]:
await self._async_internal_set_temperature(self._saved_target_temp)
# default to TURN_OFF
elif self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
elif self._window_action in [CONF_WINDOW_TURN_OFF]:
if (
self.last_central_mode != CENTRAL_MODE_STOPPED
and self.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
):
self.set_hvac_off_reason(None)
await self.restore_hvac_mode(True)
elif self._window_action in [CONF_WINDOW_FAN_ONLY]:
if self.last_central_mode != CENTRAL_MODE_STOPPED:
self.set_hvac_off_reason(None)
await self.restore_hvac_mode(True)
else:
_LOGGER.error(
@@ -2462,6 +2486,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
)
if self._window_action == CONF_WINDOW_TURN_OFF and not self.is_on:
_LOGGER.debug(
"%s is already off. Forget turning off VTherm due to window detection"
)
return
if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
if self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
self.save_hvac_mode()
@@ -2491,6 +2521,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.find_preset_temp(PRESET_ECO)
)
else: # default is to turn_off
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
await self.async_set_hvac_mode(HVACMode.OFF)
async def async_control_heating(self, force=False, _=None) -> bool:
@@ -2633,8 +2664,26 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"is_device_active": self.is_device_active,
"ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
"hvac_off_reason": self.hvac_off_reason,
}
_LOGGER_ENERGY.debug(
"%s - update_custom_attributes saved energy is %s",
self,
self.total_energy,
)
@overrides
def async_write_ha_state(self):
"""overrides to have log"""
_LOGGER_ENERGY.debug(
"%s - async_write_ha_state written state energy is %s",
self,
self._total_energy,
)
return super().async_write_ha_state()
@callback
def async_registry_entry_updated(self):
"""update the entity if the config entry have been updated

View File

@@ -17,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""

View File

@@ -109,17 +109,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
)
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
CONF_USE_MOTION_FEATURE
CONF_USE_MOTION_FEATURE, False
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG
CONF_USE_POWER_CENTRAL_CONFIG, False
) or (
self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
)
self._infos[CONF_USE_PRESENCE_FEATURE] = (
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
)
@@ -129,7 +129,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
)
self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = (
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE) is True
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True
and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
)
@@ -145,12 +145,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
):
if not is_empty:
current_config = self._infos.get(config, None)
self._infos[config] = current_config is True or (
current_config is None and self._central_config is not None
self._infos[config] = self._central_config is not None and (
current_config is True or current_config is None
)
# self._infos[config] = current_config is True or (
# current_config is None and self._central_config is not None
# )
else:
self._infos[config] = self._central_config is not None
@@ -165,7 +170,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
# check the heater_entity_id
for conf in [
CONF_HEATER,
CONF_UNDERLYING_LIST,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_WINDOW_SENSOR,
@@ -173,15 +178,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_PRESENCE_SENSOR,
CONF_CLIMATE,
]:
d = data.get(conf, None) # pylint: disable=invalid-name
if d is not None and self.hass.states.get(d) is None:
_LOGGER.error(
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
d,
)
raise UnknownEntity(conf)
if not isinstance(d, list):
d = [d]
for e in d:
if e is not None and self.hass.states.get(e) is None:
_LOGGER.error(
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
e,
)
raise UnknownEntity(conf)
# Check that only one window feature is used
ws = self._infos.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name
@@ -207,6 +214,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
# CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config
CONF_USED_BY_CENTRAL_BOILER,
]:
if data.get(conf) is True:
_LOGGER.error(
@@ -270,21 +280,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH
and infos.get(CONF_HEATER, None) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
and infos.get(CONF_CLIMATE, None) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
and infos.get(CONF_VALVE, None) is None
if infos.get(CONF_UNDERLYING_LIST, None) is not None and not infos.get(
CONF_UNDERLYING_LIST, None
):
return False
@@ -317,6 +314,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
):
return False
if (
infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI
and infos.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
and (
infos.get(CONF_TPI_COEF_INT, None) is None
or infos.get(CONF_TPI_COEF_EXT) is None
)
):
return False
if (
infos.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False) is True
and self._central_config is None
):
return False
return True
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):

View File

@@ -119,17 +119,10 @@ STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_HEATER): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN], multiple=True
),
),
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
@@ -144,17 +137,8 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(
@@ -183,17 +167,10 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_VALVE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_VALVE_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_VALVE_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_VALVE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[

View File

@@ -23,8 +23,8 @@ from .prop_algorithm import (
_LOGGER = logging.getLogger(__name__)
CONFIG_VERSION = 1
CONFIG_MINOR_VERSION = 2
CONFIG_VERSION = 2
CONFIG_MINOR_VERSION = 0
PRESET_TEMP_SUFFIX = "_temp"
PRESET_AC_SUFFIX = "_ac"
@@ -55,10 +55,7 @@ PLATFORMS: list[Platform] = [
Platform.SWITCH,
]
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_UNDERLYING_LIST = "underlying_entity_ids"
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_LAST_SEEN_TEMP_SENSOR = "last_seen_temperature_sensor_entity_id"
@@ -90,10 +87,6 @@ CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
@@ -104,10 +97,6 @@ CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
@@ -127,6 +116,20 @@ CONF_AUTO_FAN_HIGH = "auto_fan_high"
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
CONF_STEP_TEMPERATURE = "step_temperature"
# Deprecated
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
# Global params into configuration.yaml
CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_SAFETY_MODE = "safety_mode"
@@ -168,6 +171,16 @@ TYPE_AUTO_START_STOP_LEVELS = Literal[ # pylint: disable=invalid-name
AUTO_START_STOP_LEVEL_NONE,
]
HVAC_OFF_REASON_NAME = "hvac_off_reason"
HVAC_OFF_REASON_MANUAL = "manual"
HVAC_OFF_REASON_AUTO_START_STOP = "auto_start_stop"
HVAC_OFF_REASON_WINDOW_DETECTION = "window_detection"
HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name
HVAC_OFF_REASON_MANUAL,
HVAC_OFF_REASON_AUTO_START_STOP,
HVAC_OFF_REASON_WINDOW_DETECTION,
]
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec
@@ -239,10 +252,6 @@ CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
ALL_CONF = (
[
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_HEATER_KEEP_ALIVE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
@@ -272,20 +281,12 @@ ALL_CONF = (
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_AC_MODE,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
@@ -353,7 +354,11 @@ CONF_WINDOW_ACTIONS = [
CONF_WINDOW_ECO_TEMP,
]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
SUPPORT_FLAGS = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"

View File

@@ -0,0 +1,18 @@
{
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"shedding": "mdi:power-plug-off",
"safety": "mdi:shield-alert",
"none": "mdi:knob",
"frost": "mdi:snowflake"
}
}
}
}
}
}
}

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "6.5.0",
"version": "6.6.0",
"zeroconf": []
}

View File

@@ -72,21 +72,10 @@
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period",
@@ -95,21 +84,10 @@
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -309,21 +287,10 @@
"title": "Entities - {name}",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period",
@@ -332,21 +299,10 @@
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -536,7 +492,8 @@
"state": {
"power": "Shedding",
"security": "Safety",
"none": "Manual"
"none": "Manual",
"frost": "Frost"
}
}
}

View File

@@ -8,7 +8,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -49,7 +48,7 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
):
super().__init__(hass, unique_id, name)
self._attr_name = "Enable auto start/stop"
self._attr_unique_id = f"{self._device_name}_enbale_auto_start_stop"
self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
self._default_value = (
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
!= AUTO_START_STOP_LEVEL_NONE
@@ -59,7 +58,7 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
@property
def icon(self) -> str | None:
"""The icon"""
return "mdi:power-settings"
return "mdi:power-sleep"
async def async_added_to_hass(self):
await super().async_added_to_hass()

View File

@@ -20,41 +20,7 @@ from .commons import NowClass, round_to_nearest
from .base_thermostat import BaseThermostat, ConfigData
from .pi_algorithm import PITemperatureRegulator
from .const import (
overrides,
DOMAIN,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_EXPERT,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
CONF_AUTO_FAN_MODE,
CONF_AUTO_FAN_NONE,
CONF_AUTO_FAN_LOW,
CONF_AUTO_FAN_MEDIUM,
CONF_AUTO_FAN_HIGH,
CONF_AUTO_FAN_TURBO,
RegulationParamSlow,
RegulationParamLight,
RegulationParamMedium,
RegulationParamStrong,
AUTO_FAN_DTEMP_THRESHOLD,
AUTO_FAN_DEACTIVATED_MODES,
CONF_AUTO_START_STOP_LEVEL,
AUTO_START_STOP_LEVEL_NONE,
TYPE_AUTO_START_STOP_LEVELS,
UnknownEntity,
EventType,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingClimate
@@ -65,6 +31,10 @@ from .auto_start_stop_algorithm import (
)
_LOGGER = logging.getLogger(__name__)
_LOGGER_ENERGY = logging.getLogger(
"custom_components.versatile_thermostat.energy_debug"
)
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
HVACAction.COOLING,
@@ -99,10 +69,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
{
"is_over_climate",
"start_hvac_action_date",
"underlying_climate_0",
"underlying_climate_1",
"underlying_climate_2",
"underlying_climate_3",
"underlying_entities",
"regulation_accumulated_error",
"auto_regulation_mode",
"auto_fan_mode",
@@ -134,20 +101,15 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Initialize the Thermostat"""
super().post_init(config_entry)
for climate in [
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
]:
if config_entry.get(climate):
self._underlyings.append(
UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=config_entry.get(climate),
)
for climate in config_entry.get(CONF_UNDERLYING_LIST):
self._underlyings.append(
UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=climate,
)
)
self.choose_auto_regulation_mode(
config_entry.get(CONF_AUTO_REGULATION_MODE)
@@ -176,9 +138,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
)
self._auto_start_stop_level = config_entry.get(
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
)
use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
if use_auto_start_stop:
self._auto_start_stop_level = config_entry.get(
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
)
else:
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
# Instanciate the auto start stop algo
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
self._auto_start_stop_level, self.name
@@ -533,18 +500,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
self._attr_extra_state_attributes["start_hvac_action_date"] = (
self._underlying_climate_start_hvac_action_date
)
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
0
].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_climate_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_climate_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
if self.is_regulated:
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
@@ -594,6 +553,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
] = self._auto_start_stop_algo.accumulated_error_threshold
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
@@ -640,8 +600,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
if self._total_energy is None:
self._total_energy = added_energy
_LOGGER_ENERGY.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER_ENERGY.debug(
"%s - incremente_energy incremented energy is %s",
self,
self._total_energy,
)
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
@@ -882,7 +852,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
changes = True
# try to manage new target temperature set if state if no other changes have been found
if not changes:
# and if a target temperature have already been sent
if not changes and under.last_sent_temperature is not None:
_LOGGER.debug(
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
under.last_sent_temperature,
@@ -906,71 +877,83 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await end_climate_changed(changes)
async def check_auto_start_stop(self):
"""Check the auto-start-stop and an eventual action
Return False if we should stop the control_heating method"""
slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
action = self._auto_start_stop_algo.calculate_action(
self.hvac_mode,
self._saved_hvac_mode,
self.target_temperature,
self.current_temperature,
slope,
self.now,
)
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
_LOGGER.info(
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
self,
)
self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
await self.async_turn_off()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": self.name,
"cause": "Auto stop conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
# Stop here
return False
elif action == AUTO_START_STOP_ACTION_ON:
_LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
)
await self.async_turn_on()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self.update_custom_attributes()
return True
@overrides
async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _)
# Check if we need to auto start/stop the Vtherm
if (
self.auto_start_stop_enable
and self._window_auto_algo.last_slope is not None
):
action = self._auto_start_stop_algo.calculate_action(
self.hvac_mode,
self._saved_hvac_mode,
self.target_temperature,
self.current_temperature,
self._window_auto_algo.last_slope / 60, # to have the slope in °/min
self.now,
)
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
if action == AUTO_START_STOP_ACTION_OFF:
_LOGGER.info(
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
self,
)
await self.async_turn_off()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name:": self.name,
"cause": "Auto stop conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": self._window_auto_algo.last_slope,
},
)
# Stop here
if self.auto_start_stop_enable:
continu = await self.check_auto_start_stop()
if not continu:
return ret
elif action == AUTO_START_STOP_ACTION_ON:
_LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
)
await self.async_turn_on()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name:": self.name,
"cause": "Auto start conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": self._window_auto_algo.last_slope,
},
)
self.update_custom_attributes()
else:
_LOGGER.debug("%s - auto start/stop is disabled")
@@ -987,6 +970,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
def set_auto_start_stop_enable(self, is_enabled: bool):
"""Enable/Disable the auto-start/stop feature"""
self._is_auto_start_stop_enabled = is_enabled
self.update_custom_attributes()
@property
def auto_regulation_mode(self) -> str | None:

View File

@@ -10,10 +10,7 @@ from homeassistant.helpers.event import (
from homeassistant.components.climate import HVACMode
from .const import (
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_UNDERLYING_LIST,
CONF_HEATER_KEEP_ALIVE,
CONF_INVERSE_SWITCH,
overrides,
@@ -24,7 +21,9 @@ from .underlyings import UnderlyingSwitch
from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__)
_LOGGER_ENERGY = logging.getLogger(
"custom_components.versatile_thermostat.energy_debug"
)
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch."""
@@ -35,10 +34,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
{
"is_over_switch",
"is_inversed",
"underlying_switch_0",
"underlying_switch_1",
"underlying_switch_2",
"underlying_switch_3",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
@@ -90,13 +86,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
self.name,
)
lst_switches = [config_entry.get(CONF_HEATER)]
if config_entry.get(CONF_HEATER_2):
lst_switches.append(config_entry.get(CONF_HEATER_2))
if config_entry.get(CONF_HEATER_3):
lst_switches.append(config_entry.get(CONF_HEATER_3))
if config_entry.get(CONF_HEATER_4):
lst_switches.append(config_entry.get(CONF_HEATER_4))
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches):
@@ -140,16 +130,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
self._attr_extra_state_attributes["underlying_switch_0"] = under0.entity_id
self._attr_extra_state_attributes["underlying_switch_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_switch_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_switch_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
self._attr_extra_state_attributes[
"on_percent"
@@ -201,8 +185,18 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
if self._total_energy is None:
self._total_energy = added_energy
_LOGGER_ENERGY.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER_ENERGY.debug(
"%s - incremente_energy increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes()

View File

@@ -15,10 +15,7 @@ from .base_thermostat import BaseThermostat, ConfigData
from .prop_algorithm import PropAlgorithm
from .const import (
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_UNDERLYING_LIST,
# This is not really self-regulation but regulation here
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
@@ -28,7 +25,9 @@ from .const import (
from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__)
_LOGGER_ENERGY = logging.getLogger(
"custom_components.versatile_thermostat.energy_debug"
)
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
"""Representation of a class for a Versatile Thermostat over a Valve"""
@@ -37,10 +36,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
frozenset(
{
"is_over_valve",
"underlying_valve_0",
"underlying_valve_1",
"underlying_valve_2",
"underlying_valve_3",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
@@ -105,13 +101,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
self.name,
)
lst_valves = [config_entry.get(CONF_VALVE)]
if config_entry.get(CONF_VALVE_2):
lst_valves.append(config_entry.get(CONF_VALVE_2))
if config_entry.get(CONF_VALVE_3):
lst_valves.append(config_entry.get(CONF_VALVE_3))
if config_entry.get(CONF_VALVE_4):
lst_valves.append(config_entry.get(CONF_VALVE_4))
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
for _, valve in enumerate(lst_valves):
self._underlyings.append(
@@ -163,18 +153,10 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
"valve_open_percent"
] = self.valve_open_percent
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
0
].entity_id
self._attr_extra_state_attributes["underlying_valve_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_valve_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_valve_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
self._attr_extra_state_attributes[
"on_percent"
@@ -285,8 +267,18 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
if self._total_energy is None:
self._total_energy = added_energy
_LOGGER_ENERGY.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER_ENERGY.debug(
"%s - get_my_previous_state increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes()

View File

@@ -64,7 +64,7 @@
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
@@ -72,21 +72,10 @@
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period",
@@ -95,21 +84,10 @@
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -309,21 +287,10 @@
"title": "Entities - {name}",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period",
@@ -332,21 +299,10 @@
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -536,7 +492,8 @@
"state": {
"power": "Shedding",
"security": "Safety",
"none": "Manual"
"none": "Manual",
"frost": "Frost"
}
}
}
@@ -590,4 +547,4 @@
}
}
}
}
}

View File

@@ -554,7 +554,8 @@
"state": {
"power": "Délestage",
"security": "Sécurité",
"none": "Manuel"
"none": "Manuel",
"frost": "Hors Gel"
}
}
}

View File

@@ -364,7 +364,8 @@
"state": {
"power": "Ripartizione",
"security": "Sicurezza",
"none": "Manuale"
"none": "Manuale",
"frost": "Gelo"
}
}
}

View File

@@ -150,10 +150,11 @@ class VersatileThermostatAPI(dict):
return entity.state
return None
async def init_vtherm_links(self):
async def init_vtherm_links(self, entry_id=None):
"""Initialize all VTherms entities links
This method is called when HA is fully started (and all entities should be initialized)
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
If entry_id is set, only the VTherm of this entry will be reloaded
"""
await self.reload_central_boiler_binary_listener()
await self.reload_central_boiler_entities_list()
@@ -175,7 +176,8 @@ class VersatileThermostatAPI(dict):
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
):
await entity.async_startup(self.find_central_configuration())
if entry_id is None or entry_id == entity.unique_id:
await entity.async_startup(self.find_central_configuration())
async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature"""

View File

@@ -74,7 +74,7 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
}
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch",
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
@@ -82,17 +82,14 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
}
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_air_conditioner",
CONF_UNDERLYING_LIST: ["switch.mock_air_conditioner"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: True,
CONF_INVERSE_SWITCH: False,
}
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_4switch0",
CONF_HEATER_2: "switch.mock_4switch1",
CONF_HEATER_3: "switch.mock_4switch2",
CONF_HEATER_4: "switch.mock_4switch3",
CONF_UNDERLYING_LIST: ["switch.mock_4switch0", "switch.mock_4switch1","switch.mock_4switch2","switch.mock_4switch3"],
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
@@ -105,7 +102,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
}
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
@@ -115,7 +112,7 @@ MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
}
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.1,
@@ -125,13 +122,13 @@ MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
}
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE,
}
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: True,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,

View File

@@ -6,6 +6,7 @@ import logging
from unittest.mock import patch, call
from homeassistant.components.climate import HVACMode
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
@@ -14,7 +15,6 @@ from custom_components.versatile_thermostat.auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_NOTHING,
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -72,7 +72,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 4 .No change on accumulated error because the new measure is too near the last one
now = now + timedelta(minutes=1)
now = now + timedelta(seconds=11)
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
@@ -225,6 +225,7 @@ async def test_auto_start_stop_none_vtherm(
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
@@ -267,6 +268,12 @@ async def test_auto_start_stop_none_vtherm(
# 1. Vtherm auto-start/stop should be in NONE mode
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_NONE
# 2. We should not find any switch Enable entity
assert (
search_entity(hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN)
is None
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
@@ -310,6 +317,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
@@ -350,8 +358,13 @@ async def test_auto_start_stop_medium_heat_vtherm(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 15
# 1. Vtherm auto-start/stop should be in MEDIUM mode
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_MEDIUM
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
@@ -369,6 +382,8 @@ async def test_auto_start_stop_medium_heat_vtherm(
# 3. Set current temperature to 19 5 min later
now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -409,6 +424,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
# VTherm should have been stopped
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
# accumulated_error = -2.5 + target - current = -2 x 5 min / 2 capped to 5
assert vtherm._auto_start_stop_algo.accumulated_error == -5
@@ -421,12 +437,15 @@ async def test_auto_start_stop_medium_heat_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 19.0,
"current_temperature": 21.0,
"temperature_slope": 10.03,
"temperature_slope": 0.167,
"accumulated_error": -5,
"accumulated_error_threshold": 5,
},
)
]
@@ -457,6 +476,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
# VTherm should stay stopped cause slope is too low to allow the turn to On
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
# 7. Set temperature to over the target, so that it will turn to heat
now = now + timedelta(minutes=20)
@@ -472,6 +492,8 @@ async def test_auto_start_stop_medium_heat_vtherm(
# VTherm should have been stopped
assert vtherm.hvac_mode == HVACMode.HEAT
assert vtherm.hvac_off_reason is None
# a message should have been sent
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
@@ -480,12 +502,15 @@ async def test_auto_start_stop_medium_heat_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": "overClimate",
"cause": "Auto start conditions reached",
"hvac_mode": HVACMode.HEAT,
"saved_hvac_mode": HVACMode.HEAT, # saved don't change
"target_temperature": 19.0,
"current_temperature": 18.0,
"temperature_slope": -2.06,
"temperature_slope": -0.034,
"accumulated_error": 5,
"accumulated_error_threshold": 5,
},
)
]
@@ -545,9 +570,10 @@ async def test_auto_start_stop_fast_ac_vtherm(
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -599,11 +625,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.0
# VTherm should be heating
# VTherm should be cooling
assert vtherm.hvac_mode == HVACMode.COOL
# 3. Set current temperature to 19 5 min later
now = now + timedelta(minutes=5)
# reset accumulated error for test
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -611,7 +639,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
await send_temperature_change_event(vtherm, 25, now, True)
await hass.async_block_till_done()
# VTherm should still be heating
# VTherm should still be cooling
assert vtherm.hvac_mode == HVACMode.COOL
assert mock_send_event.call_count == 0
assert (
@@ -641,12 +669,15 @@ async def test_auto_start_stop_fast_ac_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.COOL,
"target_temperature": 25.0,
"current_temperature": 23.0,
"temperature_slope": -16.8,
"temperature_slope": -0.28,
"accumulated_error": 2,
"accumulated_error_threshold": 2,
},
)
]
@@ -664,18 +695,18 @@ async def test_auto_start_stop_fast_ac_vtherm(
)
# 5. Set temperature to over the target, but slope is too low -> no change
now = now + timedelta(minutes=20)
now = now + timedelta(minutes=30)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 26, now, True)
await send_temperature_change_event(vtherm, 25.5, now, True)
await hass.async_block_till_done()
# accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2
assert vtherm._auto_start_stop_algo.accumulated_error == -2
# VTherm should have been stopped
# VTherm should stay stopped
assert vtherm.hvac_mode == HVACMode.OFF
# a message should have been sent
assert mock_send_event.call_count == 0
@@ -702,12 +733,15 @@ async def test_auto_start_stop_fast_ac_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": "overClimate",
"cause": "Auto start conditions reached",
"hvac_mode": HVACMode.COOL,
"saved_hvac_mode": HVACMode.COOL, # saved don't change
"target_temperature": 25.0,
"current_temperature": 26.5,
"temperature_slope": 5.74,
"temperature_slope": 0.112,
"accumulated_error": -2,
"accumulated_error_threshold": 2,
},
)
]
@@ -767,6 +801,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
@@ -826,6 +861,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
# 3. Set current temperature to 21 5 min later to auto-stop
now = now + timedelta(minutes=5)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -846,12 +882,15 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 17.0,
"current_temperature": 19.0,
"temperature_slope": 18.0,
"temperature_slope": 0.3,
"accumulated_error": -2,
"accumulated_error_threshold": 2,
},
)
]
@@ -900,12 +939,15 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": "overClimate",
"cause": "Auto start conditions reached",
"hvac_mode": HVACMode.HEAT,
"saved_hvac_mode": HVACMode.HEAT, # saved don't change
"target_temperature": 21.0,
"current_temperature": 17.0,
"temperature_slope": -5.19,
"temperature_slope": -0.087,
"accumulated_error": 2,
"accumulated_error_threshold": 2,
},
)
]
@@ -921,3 +963,503 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
)
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than auto-start/stop restart a VTherm stopped upon preset_change (in fast mode)"""
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in FAST mode and enable should be on
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_enable") is True
# 2. set enable to false
enable_entity.turn_off()
await hass.async_block_till_done()
assert enable_entity.state == STATE_OFF
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_enable") is False
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 3. Set mode to Heat and preset to Comfort
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 16, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_ECO)
await hass.async_block_till_done()
assert vtherm.target_temperature == 17.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# 3. Set current temperature to 21 5 min later to auto-stop
now = now + timedelta(minutes=5)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 19, now, True)
await hass.async_block_till_done()
# VTherm should not have been stopped cause enable is false
assert vtherm.hvac_mode == HVACMode.HEAT
# Not calculated cause enable = false
assert vtherm._auto_start_stop_algo.accumulated_error == 0
# a message should have been sent
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_fast_heat_window(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than auto-start/stop works with a real over_climate VTherm in FAST level and check
interaction with window openess detection"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 2. Set mode to Heat and preset to Comfort and close the window
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
assert vtherm.window_state == STATE_OFF
# 3. Set current temperature to 21 5 min later -> should turn off VTherm
now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 21, now, True)
await hass.async_block_till_done()
# VTherm should no more be heating
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2
# 4. Open the window and wait for the delay
now = now + timedelta(minutes=2)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
vtherm._set_now(now)
try_function = await send_window_change_event(
vtherm, True, False, now, sleep=False
)
await try_function(None)
# Nothing should have change (window event is ignoed as we are already OFF)
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
assert vtherm._saved_hvac_mode == HVACMode.HEAT
mock_send_event.assert_not_called()
assert vtherm.window_state == STATE_ON
# 5. close the window
now = now + timedelta(minutes=2)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
vtherm._set_now(now)
try_function = await send_window_change_event(
vtherm, False, True, now, sleep=False
)
await try_function(None)
# The VTherm should stay off because the reason of off is auto-start-stop
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 0
assert vtherm.window_state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_fast_heat_window_mixed(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than auto-start/stop works with a real over_climate VTherm in FAST level and check
interaction with window openess detection
The case is when first window on, then auto-stop, then window off and then auto-start
"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 2. Set mode to Heat and preset to Comfort and close the window
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
assert vtherm.window_state == STATE_OFF
# 3. Open the window and wait for the delay
now = now + timedelta(minutes=2)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
vtherm._set_now(now)
try_function = await send_window_change_event(
vtherm, True, False, now, sleep=False
)
await try_function(None)
# Nothing should have change (window event is ignoed as we are already OFF)
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2
assert vtherm.window_state == STATE_ON
# 4. Set current temperature to 21 5 min later -> should turn off VTherm
now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 21, now, True)
await hass.async_block_till_done()
# VTherm should no more be heating
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION # No change
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 0 # No message
# 5. close the window
now = now + timedelta(minutes=2)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
vtherm._set_now(now)
try_function = await send_window_change_event(
vtherm, False, True, now, sleep=False
)
await try_function(None)
# The VTherm should turn on and off again due to auto-start-stop
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason is HVAC_OFF_REASON_AUTO_START_STOP
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert vtherm.window_state == STATE_OFF
assert mock_send_event.call_count >= 2
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 19.0,
"current_temperature": 21.0,
"temperature_slope": 0.214,
"accumulated_error": -2,
"accumulated_error_threshold": 2,
},
),
]
)

View File

@@ -630,6 +630,7 @@ async def test_climate_ac_only_change_central_mode_true(
},
)
# 1. set hvac_mode to COOL and preet ECO
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
@@ -982,7 +983,8 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
assert entity.last_central_mode is CENTRAL_MODE_COOL_ONLY
await entity.async_set_hvac_mode(HVACMode.OFF)
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_off_reason == HVAC_OFF_REASON_MANUAL
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
@@ -1000,12 +1002,14 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
await try_function(None)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
)
# The VTherm is already off -> window detection is ignored
assert mock_send_event.call_count == 0
# mock_send_event.assert_has_calls(
# [call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
# )
assert entity.hvac_mode == HVACMode.OFF
assert entity.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
@@ -1021,6 +1025,8 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
assert entity.last_central_mode is CENTRAL_MODE_AUTO
# No change
assert entity.hvac_mode == HVACMode.OFF
# We have to a reason of WINDOW_DETECTION
assert entity.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
@@ -1046,6 +1052,7 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
# We should stay off because central is STOPPED
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_off_reason is None
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
@@ -320,6 +322,78 @@ async def test_bug_101(
assert entity.preset_mode is PRESET_NONE
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_615(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm don't change its own temperature target if no
target_temperature have already been sent"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
)
# Underlying is in HEAT mode but should be shutdown at startup
fake_underlying_climate = MockClimate(
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
)
# 1. create the thermostat
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert vtherm
assert vtherm.name == "TheOverClimateMockName"
assert vtherm.is_over_climate is True
assert vtherm.hvac_mode is HVACMode.OFF
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert vtherm.hvac_action is HVACAction.HEATING
# Force a preset_mode without sending a temperature (as it was restored with a preset)
vtherm._attr_preset_mode = PRESET_BOOST
assert vtherm.target_temperature == vtherm.min_temp
assert vtherm.preset_mode is PRESET_BOOST
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
# 2. Change the target temp of underlying thermostat at now + 1 min
now = now + timedelta(minutes=1)
await send_climate_change_event_with_temperature(
vtherm,
HVACMode.OFF,
HVACMode.OFF,
HVACAction.OFF,
HVACAction.OFF,
now,
25,
True,
"climate.mock_climate", # the underlying climate entity id
)
# Should NOT have been taken the new target temp nor have change the preset
assert vtherm.target_temperature == vtherm.min_temp
assert vtherm.preset_mode is PRESET_BOOST
mock_underlying_set_hvac_mode.assert_not_called()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_508(
hass: HomeAssistant,
@@ -631,3 +705,356 @@ async def test_ignore_temp_outside_minmax_range(
"climate.mock_climate", # the underlying climate entity id
)
assert entity.target_temperature == 17
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_manual_hvac_off_should_take_the_lead_over_window(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than a manual hvac_off is taken into account over a window hvac_off"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort and close the window
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
assert vtherm.window_state == STATE_OFF
# 2. Open the window and wait for the delay
now = now + timedelta(minutes=2)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
vtherm._set_now(now)
try_function = await send_window_change_event(
vtherm, True, False, now, sleep=False
)
await try_function(None)
# Nothing should have change (window event is ignoed as we are already OFF)
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2
assert vtherm.window_state == STATE_ON
# 3. Turn off manually the VTherm. This should be taken into account
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await vtherm.async_set_hvac_mode(HVACMode.OFF)
await hass.async_block_till_done()
# Should be off with reason MANUAL
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
# Window state should not change
assert vtherm.window_state == STATE_ON
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
]
)
# 4. close the window -> we should stay off reason manual
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
try_function = await send_window_change_event(
vtherm, False, True, now, sleep=False
)
await try_function(None)
# The VTherm should turn on and off again due to auto-start-stop
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason is HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
assert vtherm.window_state == STATE_OFF
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than a manual hvac_off is taken into account over a auto-start/stop hvac_off"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# 2. Set current temperature to 21 5 min later -> should turn off VTherm
now = now + timedelta(minutes=5)
vtherm._set_now(now)
# reset accumulated error (only for testing)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await send_temperature_change_event(vtherm, 21, now, True)
await hass.async_block_till_done()
# VTherm should no more be heating
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2 # turned to off
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 19.0,
"current_temperature": 21.0,
"temperature_slope": 0.3,
"accumulated_error": -2,
"accumulated_error_threshold": 2,
},
),
]
)
# 3. Turn off manually the VTherm. This should be taken into account
now = now + timedelta(minutes=1)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await vtherm.async_set_hvac_mode(HVACMode.OFF)
await hass.async_block_till_done()
# Should be off with reason MANUAL
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
]
)
# 4. removes the auto-start/stop detection
now = now + timedelta(minutes=5)
vtherm._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
await send_temperature_change_event(vtherm, 15, now, True)
await hass.async_block_till_done()
# VTherm should no more be heating
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
assert mock_send_event.call_count == 0 # nothing have change

View File

@@ -205,6 +205,8 @@ async def test_window_management_time_enough(
assert mock_heater_off.call_count == 2
assert mock_condition.call_count == 1
assert entity.hvac_mode is HVACMode.OFF
assert entity._saved_hvac_mode is HVACMode.HEAT
assert entity.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert entity.window_state == STATE_ON
# Close the window
@@ -242,6 +244,9 @@ async def test_window_management_time_enough(
any_order=False,
)
assert entity.preset_mode is PRESET_BOOST
assert entity.hvac_mode is HVACMode.HEAT
assert entity._saved_hvac_mode is HVACMode.HEAT # No change
assert entity.hvac_off_reason == None
# Clean the entity
entity.remove_thermostat()
@@ -1339,6 +1344,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
# The underlying should be in FAN_ONLY hvac_mode
assert entity.hvac_mode is HVACMode.FAN_ONLY
assert entity._saved_hvac_mode is HVACMode.HEAT
assert entity.hvac_off_reason is None # Hvac is not off
assert entity.preset_mode is PRESET_COMFORT
# 3. Close the window
@@ -1357,7 +1363,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
await hass.async_block_till_done()
assert entity.window_state == STATE_OFF
assert mock_send_event.call_count == 1
@@ -1379,6 +1385,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.hvac_off_reason is None
# Clean the entity
entity.remove_thermostat()