Compare commits
15 Commits
6.6.1.beta
...
6.8.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a658b7a2a | ||
|
|
289ccc7bb7 | ||
|
|
c1d1e8f1db | ||
|
|
71c35ecdc0 | ||
|
|
4f8e45dda6 | ||
|
|
d624c327b6 | ||
|
|
b46a24f834 | ||
|
|
d31376d55d | ||
|
|
dbfd294ff3 | ||
|
|
e111bd0647 | ||
|
|
ba69319198 | ||
|
|
f9df925181 | ||
|
|
2d72efe447 | ||
|
|
95af6eba97 | ||
|
|
06dc537767 |
@@ -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
|
||||
|
||||
59
README-fr.md
59
README-fr.md
@@ -13,6 +13,7 @@
|
||||
- [Dans le cas d'une configuration centrale](#dans-le-cas-dune-configuration-centrale)
|
||||
- [Refonte du menu de configuration](#refonte-du-menu-de-configuration)
|
||||
- [Les options de menu 'Configuration incomplète' et 'Finaliser'](#les-options-de-menu-configuration-incomplète-et-finaliser)
|
||||
- [Changements dans la version 5.0](#changements-dans-la-version-50)
|
||||
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
|
||||
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
|
||||
- [Incompatibilités](#incompatibilités)
|
||||
@@ -130,6 +131,8 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Changements dans la version 6.0</summary>
|
||||
# Changements dans la version 6.0
|
||||
|
||||
## Entités de température pour les pre-réglages
|
||||
@@ -197,10 +200,13 @@ Une fois que toute la configuration est valide, la dernière option se transform
|
||||
Cliquez sur cette option pour créér (resp. modifier) le VTherm :
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Changements dans la version 5.0</summary>
|
||||
|
||||
# Changements dans la version 5.0
|
||||
|
||||
Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez :
|
||||
1. Créer un VTherm de type "Configuration Centrale",
|
||||
2. Saisir les attributs de cette configuration centrale
|
||||
@@ -241,6 +247,7 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
|
||||
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
|
||||
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
|
||||
6. La clim Airwell avec l'intégration "Midea AC LAN". Si 2 commandes de VTherm sont trop rapprochées, la clim s'arrête d'elle même.
|
||||
7. Les climates basés sur l'intégration Overkiz ne fonctionnent pas. Il parait impossible d'éteindre ni même de changer la température sur ces systèmes.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
@@ -291,9 +298,6 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
||||
> 3. En plus de cette configuration centralisée, tous les VTherm peuvent être contrôlées par une seule entité de type `select`. Cette fonction est nommé `central_mode`. Cela permet de stopper / démarrer / mettre en hors gel / etc tous les VTherms en une seule fois. Pour chaque VTherm, l'utilisateur indique si il est concerné par ce `central_mode`.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Création d'un nouveau Versatile Thermostat</summary>
|
||||
|
||||
## Création d'un nouveau Versatile Thermostat
|
||||
|
||||
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
|
||||
@@ -307,10 +311,6 @@ puis
|
||||
La configuration peut être modifiée via la même interface. Sélectionnez simplement le thermostat à modifier, appuyez sur "Configurer" et vous pourrez modifier certains paramètres ou la configuration.
|
||||
|
||||
Suivez ensuite les étapes de configuration en sélectionnant dans le menu l'option à configurer.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Choix des attributs de base</summary>
|
||||
|
||||
## Choix des attributs de base
|
||||
|
||||
@@ -332,10 +332,6 @@ Donnez les principaux attributs obligatoires :
|
||||
>  _*Notes*_
|
||||
> 1. avec les types ```over_switch``` et ```over_valve```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
|
||||
> 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Sélectionnez des entités pilotées (sous-jacents)</summary>
|
||||
|
||||
## Sélectionnez des entités pilotées (sous-jacents)
|
||||
|
||||
@@ -531,10 +527,6 @@ Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number
|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
|
||||
|
||||
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurez les coefficients de l'algorithme TPI</summary>
|
||||
|
||||
## Configurez les coefficients de l'algorithme TPI
|
||||
|
||||
@@ -548,10 +540,6 @@ Vous devez donner :
|
||||
|
||||
|
||||
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer les températures préréglées</summary>
|
||||
|
||||
## Configurer les températures préréglées
|
||||
|
||||
@@ -572,10 +560,6 @@ Les pré-réglages se font (depuis v6.0) directement depuis les entités du VThe
|
||||
> 3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
|
||||
> 4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
|
||||
> 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer les portes/fenêtres en allumant/éteignant les thermostats</summary>
|
||||
|
||||
## Configurer les portes/fenêtres en allumant/éteignant les thermostats
|
||||
|
||||
@@ -618,10 +602,6 @@ Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvert
|
||||
> 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
|
||||
> 3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
|
||||
> 4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer le mode d'activité ou la détection de mouvement</summary>
|
||||
|
||||
## Configurer le mode d'activité ou la détection de mouvement
|
||||
|
||||
@@ -649,10 +629,6 @@ Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activ
|
||||
|
||||
>  _*Notes*_
|
||||
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer la gestion de la puissance</summary>
|
||||
|
||||
## Configurer la gestion de la puissance
|
||||
|
||||
@@ -671,10 +647,6 @@ Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'u
|
||||
> 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
|
||||
> 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
|
||||
> 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer la présence (ou l'absence)</summary>
|
||||
|
||||
## Configurer la présence (ou l'absence)
|
||||
|
||||
@@ -696,10 +668,6 @@ ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de
|
||||
>  _*Notes*_
|
||||
> 1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
|
||||
> 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configuration avancée</summary>
|
||||
|
||||
## Configuration avancée
|
||||
|
||||
@@ -717,6 +685,8 @@ Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque
|
||||
|
||||
Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
|
||||
|
||||
Note: les paramètres `security_min_on_percent` et `security_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
|
||||
|
||||
Depuis la version 5.3 il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
|
||||
```
|
||||
versatile_thermostat:
|
||||
@@ -734,10 +704,6 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
|
||||
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
|
||||
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
|
||||
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Le contrôle centralisé</summary>
|
||||
|
||||
## Le contrôle centralisé
|
||||
|
||||
@@ -754,10 +720,6 @@ Il est donc possible de contrôler tous les VTherms (que ceux que l'on désigne
|
||||
Exemple de rendu :
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Le contrôle d'une chaudière centrale</summary>
|
||||
|
||||
## Le contrôle d'une chaudière centrale
|
||||
|
||||
@@ -859,7 +821,6 @@ context:
|
||||
|
||||
>  _*Notes*_
|
||||
> Le contrôle par du logiciel ou du matériel de type domotique d'une chaudière centrale peut induire des risques pour son bon fonctionnement. Assurez-vous avant d'utiliser ces fonctions, que votre chaudière possède bien des fonctions de sécurité et que celles-ci fonctionnent. Allumer une chaudière si tous les robinets sont fermés peut générer de la sur-pression par exemple.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Synthèse des paramètres</summary>
|
||||
@@ -1610,7 +1571,7 @@ Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les u
|
||||
<summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
|
||||
|
||||
## Pourquoi mon Versatile Thermostat se met en Securite ?
|
||||
Le mode sécurité n'est possible que sur les VTherm `over_switch` et `over_valve`. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
|
||||
Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
|
||||
|
||||
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
|
||||
|
||||
|
||||
278
README.md
278
README.md
@@ -13,7 +13,7 @@
|
||||
- [In the case of a central configuration](#in-the-case-of-a-central-configuration)
|
||||
- [Redesign of the configuration menu](#redesign-of-the-configuration-menu)
|
||||
- [The 'Incomplete configuration' and 'Finalize' menu options](#the-incomplete-configuration-and-finalize-menu-options)
|
||||
- [Changements dans la version 5.0](#changements-dans-la-version-50)
|
||||
- [Changes in release 5.0](#changes-in-release-50)
|
||||
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
|
||||
- [When to use / not use](#when-to-use--not-use)
|
||||
- [Incompatibilities](#incompatibilities)
|
||||
@@ -130,6 +130,10 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
|
||||
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Changes in version 6.0</summary>
|
||||
|
||||
# Changes in version 6.0
|
||||
|
||||
## Temperature entities for presets
|
||||
@@ -197,11 +201,12 @@ Once all configuration is valid, the last option changes to:
|
||||
Click on this option to create (resp. modify) the VTherm:
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Changements dans la version 5.0</summary>
|
||||
<summary>Changes in release 5.0</summary>
|
||||
|
||||
# Changements dans la version 5.0
|
||||
# Changes in release 5.0
|
||||
|
||||
You can now define a central configuration which will allow you to share certain attributes on all your VTherms (or only part of them). To use this possibility, you must:
|
||||
1. Create a VTherm of type “Central Configuration”,
|
||||
@@ -242,6 +247,7 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
|
||||
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
|
||||
5. TRV of type Aqara SRTS-A01 and MOES TV01-ZB which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
|
||||
6. The Airwell with the "Midea AC LAN" integration. If two orders are too close, the device shut off.
|
||||
7. System based on intégration Overkiz don't work as expected. It seems not possible to turn off nor sending setpoint on those systems.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
@@ -292,9 +298,6 @@ This component named __Versatile thermostat__ manage the following use cases :
|
||||
> 3. In addition to this centralized configuration, all VTherms can be controlled by a single entity of type `select`. This function is named `central_mode`. This allows you to stop / start / freeze / etc. all VTherms at once. For each VTherm, the user indicates whether he is affected by this `central_mode`.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Creation of a new Versatile Thermostat</summary>
|
||||
|
||||
## Creation of a new Versatile Thermostat
|
||||
|
||||
Click on Add integration button in the integration page
|
||||
@@ -305,11 +308,6 @@ The configuration can be change through the same interface. Simply select the th
|
||||
Then choose the type of VTherm you want to create:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Minimal configuration update</summary>
|
||||
|
||||
## Minimal configuration update
|
||||
|
||||
Then choose the “Main attributes” menu.
|
||||
@@ -330,10 +328,6 @@ Give the main mandatory attributes:
|
||||
>  _*Notes*_
|
||||
> 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
|
||||
> 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Select the driven entity</summary>
|
||||
|
||||
## Select the driven entity
|
||||
|
||||
@@ -395,82 +389,6 @@ These three parameters make it possible to modulate the regulation and avoid mul
|
||||
|
||||
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
|
||||
|
||||
#### Self-regulation in Expert mode
|
||||
|
||||
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
|
||||
The adjustable parameters are as follows:
|
||||
1. `kp`: the factor applied to the raw error,
|
||||
2. `ki`: the factor applied to the accumulation of errors,
|
||||
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
|
||||
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
|
||||
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
|
||||
6. `accumulated_error_threshold`: the maximum for error accumulation.
|
||||
|
||||
For tuning, these observations must be taken into account:
|
||||
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
|
||||
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
|
||||
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
|
||||
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
|
||||
|
||||
The pre-programmed values are as follows:
|
||||
|
||||
Slow régulation :
|
||||
|
||||
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
|
||||
Light régulation :
|
||||
|
||||
kp: 0.2
|
||||
ki: 0.05
|
||||
k_ext: 0.05
|
||||
offset_max: 1.5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 10
|
||||
|
||||
Medium régulation :
|
||||
|
||||
kp: 0.3
|
||||
ki: 0.05
|
||||
k_ext: 0.1
|
||||
offset_max: 2
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 20
|
||||
|
||||
Strong régulation :
|
||||
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
|
||||
```
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
```
|
||||
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
|
||||
|
||||
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
|
||||
|
||||
#### Internal temperature compensation
|
||||
Sometimes, a device’s internal temperature sensor (like in a TRV or AC) can give inaccurate readings, especially if it’s too close to a heat source. This can cause the device to stop heating too soon.
|
||||
For example:
|
||||
@@ -522,11 +440,6 @@ The algorithm to use is currently limited to TPI is available. See [algorithm](#
|
||||
|
||||
It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the TPI algorithm coefficients</summary>
|
||||
|
||||
## Configure the TPI algorithm coefficients
|
||||
|
||||
Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the "TPI" menu option, you will get there:
|
||||
@@ -534,11 +447,6 @@ Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the
|
||||
|
||||
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the preset temperature</summary>
|
||||
|
||||
## Configure the preset temperature
|
||||
|
||||
The preset mode allows you to pre-configurate targeted temperature. Used in conjonction with Scheduler (see [scheduler](#even-better-with-scheduler-component) you will have a powerfull and simple way to optimize the temperature vs electrical consumption of your hous. Preset handled are the following :
|
||||
@@ -559,11 +467,6 @@ The pre-settings are made (since v6.0) directly from the VTherm entities or from
|
||||
> 4. if you uses the advanced configuration you will see the preset set to ``safety`` if the temperature could not be retrieved after a certain delay
|
||||
> 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the doors/windows turning on/off the thermostats</summary>
|
||||
|
||||
## Configure the doors/windows turning on/off the thermostats
|
||||
|
||||
You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
|
||||
@@ -605,11 +508,6 @@ And that's all ! your thermostat will turn off when the windows are open and tur
|
||||
> 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
|
||||
> 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the activity mode or motion detection</summary>
|
||||
|
||||
## Configure the activity mode or motion detection
|
||||
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
|
||||

|
||||
@@ -634,11 +532,6 @@ For this to work, the climate thermostat should be in ``Activity`` preset mode.
|
||||
>  _*Notes*_
|
||||
> 1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the power management</summary>
|
||||
|
||||
## Configure the power management
|
||||
|
||||
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
|
||||
@@ -656,10 +549,6 @@ This allows you to change the max power along time using a Scheduler or whatever
|
||||
> 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
|
||||
> 4. If you don't want to use this feature, just leave the entities id empty
|
||||
> 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure presence or occupancy</summary>
|
||||
|
||||
## Configure presence or occupancy
|
||||
|
||||
@@ -682,11 +571,6 @@ ATTENTION: groups of people do not function as a presence sensor. They are not r
|
||||
> 1. the change in temperature is immediate and is reflected on the front shutter. The calculation will take into account the new target temperature at the next calculation of the cycle,
|
||||
> 2. you can use the person.xxxx direct sensor or a group of Home Assistant sensors. The presence sensor manages the ``on`` or ``home`` states as present and the ``off`` or ``not_home`` states as absent.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Advanced configuration</summary>
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
Those parameters allows to fine tune the thermostat.
|
||||
@@ -703,6 +587,8 @@ Setting this parameter to ``0.00`` will trigger the safety preset regardless of
|
||||
|
||||
The fourth parameter (``security_default_on_percent``) is the ``on_percent`` value that will be used when the thermostat enters ``safety`` mode. If you put ``0`` then the thermostat will be cut off when it goes into ``safety`` mode, putting 0.2% for example allows you to keep a little heating (20% in this case), even in mode ``safety``. It avoids finding your home totally frozen during a thermometer failure.
|
||||
|
||||
Note: parameters `security_min_on_percent` et `security_default_on_percent` are not used by `over_climate` VTherm.
|
||||
|
||||
Since version 5.3 it is possible to deactivate the safety device following a lack of data from the outdoor thermometer. Indeed, this most of the time having a low impact on regulation (depending on your settings), it is possible that it is absent without endangering the home. To do this, you must add the following lines to your `configuration.yaml`:
|
||||
```
|
||||
versatile_thermostat:
|
||||
@@ -721,11 +607,6 @@ See [example tuning](#examples-tuning) for common tuning examples
|
||||
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
|
||||
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Centralized control</summary>
|
||||
|
||||
## Centralized control
|
||||
|
||||
Since release 5.2, if you have defined a centralized configuration, you have a new entity named `select.central_mode` which allows you to control all VTherms with a single action. For a VTherm to be centrally controllable, its configuration attribute named `use_central_mode` must be true.
|
||||
@@ -742,11 +623,6 @@ Example rendering:
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Control of a central boiler</summary>
|
||||
|
||||
## Control of a central boiler
|
||||
|
||||
Since release 5.3, you have the possibility of controlling a centralized boiler. From the moment it is possible to start or stop this boiler from Home Assistant, then Versatile Thermostat will be able to control it directly.
|
||||
@@ -848,7 +724,112 @@ context:
|
||||
>  _*Notes*_
|
||||
> Controlling a central boiler using software or hardware such as home automation can pose risks to its proper functioning. Before using these functions, make sure that your boiler has safety functions and that they are working. Turning on a boiler if all the taps are closed can generate excess pressure, for example.
|
||||
|
||||
</details>
|
||||
|
||||
## Expert Mode Settings
|
||||
|
||||
Expert Mode settings refer to Settings made in the Home Assistant `configuration.yaml` file under the `versatile_thermostat` section. You might have to add this section by yourself to the `configuration.yaml` file.
|
||||
|
||||
These settings are meant to be used only in **specific niche cases and with careful considerations**.
|
||||
|
||||
|
||||
The following sections describe the available export mode settings in detail with examples on how to configure them. Be aware that these settings require a **complete restart** of Home Assistant or a **reload of Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat) to take effect.
|
||||
|
||||
|
||||
### Self-regulation in Expert mode
|
||||
|
||||
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
|
||||
The adjustable parameters are as follows:
|
||||
1. `kp`: the factor applied to the raw error,
|
||||
2. `ki`: the factor applied to the accumulation of errors,
|
||||
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
|
||||
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
|
||||
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
|
||||
6. `accumulated_error_threshold`: the maximum for error accumulation.
|
||||
|
||||
For tuning, these observations must be taken into account:
|
||||
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
|
||||
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
|
||||
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
|
||||
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
|
||||
|
||||
The pre-programmed values are as follows:
|
||||
|
||||
Slow régulation :
|
||||
|
||||
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
|
||||
Light régulation :
|
||||
|
||||
kp: 0.2
|
||||
ki: 0.05
|
||||
k_ext: 0.05
|
||||
offset_max: 1.5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 10
|
||||
|
||||
Medium régulation :
|
||||
|
||||
kp: 0.3
|
||||
ki: 0.05
|
||||
k_ext: 0.1
|
||||
offset_max: 2
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 20
|
||||
|
||||
Strong régulation :
|
||||
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
|
||||
```
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
```
|
||||
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
|
||||
|
||||
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
|
||||
|
||||
|
||||
### On Time Clamping (max_on_percent)
|
||||
|
||||
|
||||
The calculated on time percent can be limited to a maximum percentage of the cycle duration. This setting has to be made in expert mode and will be used for all Versatile Thermostats.
|
||||
|
||||
```
|
||||
versatile_thermostat:
|
||||
max_on_percent: 0.8
|
||||
```
|
||||
|
||||
The example above limits the maximum ON time to 80% (0.8) of the cycle length. If the cycle length is for example 600 seconds (10min), the maximum ON time will be limited to 480 seconds (8min). The remaining 120 seconds of the cycle will always remain in the OFF state.
|
||||
|
||||
There are three debug attributes of interest regarding this feature:
|
||||
|
||||
* `max_on_percent` # clamping setting as configured in expert mode
|
||||
* `calculated_on_percent` # calculated on percent without clamping applied
|
||||
* `on_percent` # used on percent with clamping applied
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Parameter summary</summary>
|
||||
@@ -1298,9 +1279,13 @@ Replace values in [[ ]] by yours.
|
||||
yaxis: y1
|
||||
name: Ema
|
||||
- entity: '[[climate]]'
|
||||
attribute: regulated_target_temperature
|
||||
yaxis: y1
|
||||
name: Regulated T°
|
||||
attribute: on_percent
|
||||
yaxis: y2
|
||||
name: Power percent
|
||||
fill: tozeroy
|
||||
fillcolor: rgba(200, 10, 10, 0.3)
|
||||
line:
|
||||
color: rgba(200, 10, 10, 0.9)
|
||||
- entity: '[[slope]]'
|
||||
name: Slope
|
||||
fill: tozeroy
|
||||
@@ -1325,12 +1310,19 @@ Replace values in [[ ]] by yours.
|
||||
yaxis:
|
||||
visible: true
|
||||
position: 0
|
||||
yaxis2:
|
||||
visible: true
|
||||
position: 0
|
||||
fixedrange: true
|
||||
range:
|
||||
- 0
|
||||
- 1
|
||||
yaxis9:
|
||||
visible: true
|
||||
fixedrange: false
|
||||
range:
|
||||
- -0.5
|
||||
- 0.5
|
||||
- -2
|
||||
- 2
|
||||
position: 1
|
||||
xaxis:
|
||||
rangeselector:
|
||||
@@ -1353,7 +1345,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).
|
||||
|
||||
@@ -1596,7 +1588,7 @@ These parameters are sensitive and quite difficult to adjust. Please only use th
|
||||
|
||||
## Why does my Versatile Thermostat go into Safety?
|
||||
|
||||
Safety mode is only possible on VTherm `over_switch` and `over_valve`. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`.
|
||||
Safety mode is possible on all VTherm's type. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`.
|
||||
|
||||
As the algorithm is based on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To avoid this, when the conditions mentioned above are detected, heating is limited to the `security_default_on_percent` parameter. This value must therefore be reasonably low. It helps prevent a fire while avoiding completely cutting off the radiator (risk of freezing).
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ from .const import (
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
@@ -86,6 +87,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
|
||||
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
|
||||
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
|
||||
vol.Optional(CONF_MAX_ON_PERCENT): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
},
|
||||
@@ -178,13 +180,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:
|
||||
@@ -193,7 +202,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:
|
||||
|
||||
@@ -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
|
||||
@@ -84,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."""
|
||||
|
||||
@@ -134,7 +141,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"is_device_active",
|
||||
"target_temperature_step",
|
||||
"is_used_by_central_boiler",
|
||||
"temperature_slope"
|
||||
"temperature_slope",
|
||||
"max_on_percent"
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -198,6 +206,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
|
||||
@@ -470,6 +479,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
|
||||
@@ -498,6 +508,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||
)
|
||||
|
||||
self._max_on_percent = api._max_on_percent
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||
self,
|
||||
@@ -585,14 +597,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)
|
||||
@@ -804,6 +826,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
|
||||
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:
|
||||
@@ -817,6 +844,11 @@ 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,
|
||||
)
|
||||
|
||||
if not self._hvac_mode:
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
@@ -1177,6 +1209,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
|
||||
@@ -1198,14 +1248,11 @@ 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(
|
||||
@@ -2198,8 +2245,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
save_all()
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_STOPPED:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
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:
|
||||
@@ -2213,7 +2261,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
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
|
||||
@@ -2620,8 +2669,25 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"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,
|
||||
"max_on_percent": self._max_on_percent,
|
||||
}
|
||||
|
||||
_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
|
||||
|
||||
@@ -17,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -209,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(
|
||||
@@ -306,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):
|
||||
|
||||
@@ -133,6 +133,7 @@ CONF_VALVE_4 = "valve_entity4_id"
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
CONF_SAFETY_MODE = "safety_mode"
|
||||
CONF_MAX_ON_PERCENT = "max_on_percent"
|
||||
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
|
||||
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
|
||||
@@ -354,7 +355,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"
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.6.0",
|
||||
"version": "6.7.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class PropAlgorithm:
|
||||
cycle_min: int,
|
||||
minimal_activation_delay: int,
|
||||
vtherm_entity_id: str = None,
|
||||
max_on_percent: float = None,
|
||||
) -> None:
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
@@ -78,6 +79,7 @@ class PropAlgorithm:
|
||||
self._off_time_sec = self._cycle_min * 60
|
||||
self._security = False
|
||||
self._default_on_percent = 0
|
||||
self._max_on_percent = max_on_percent
|
||||
|
||||
def calculate(
|
||||
self,
|
||||
@@ -161,6 +163,15 @@ class PropAlgorithm:
|
||||
)
|
||||
self._on_percent = self._calculated_on_percent
|
||||
|
||||
if self._max_on_percent is not None and self._on_percent > self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"%s - Heating period clamped to %s (instead of %s) due to max_on_percent setting.",
|
||||
self._vtherm_entity_id,
|
||||
self._max_on_percent,
|
||||
self._on_percent,
|
||||
)
|
||||
self._on_percent = self._max_on_percent
|
||||
|
||||
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
|
||||
@@ -34,10 +34,16 @@ async def async_setup_entry(
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE and auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
async_add_entities([enable_entity], True)
|
||||
entities = []
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
|
||||
|
||||
if auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
entities.append(enable_entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
||||
@@ -100,3 +106,63 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
|
||||
def turn_on(self, **kwargs: Any):
|
||||
self._attr_is_on = True
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
|
||||
class FollowUnderlyingTemperatureChange(
|
||||
VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity
|
||||
):
|
||||
"""The that enables the ManagedDevice optimisation with"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
|
||||
):
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Follow underlying temp change"
|
||||
self._attr_unique_id = f"{self._device_name}_follow_underlying_temp_change"
|
||||
self._attr_is_on = False
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:content-copy"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Récupérer le dernier état sauvegardé de l'entité
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
# Si l'état précédent existe, vous pouvez l'utiliser
|
||||
if last_state is not None:
|
||||
self._attr_is_on = last_state.state == "on"
|
||||
else:
|
||||
# If no previous state set it to false by default
|
||||
self._attr_is_on = False
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
def update_my_state_and_vtherm(self):
|
||||
"""Update the follow flag in my VTherm"""
|
||||
self.async_write_ha_state()
|
||||
if self.my_climate is not None:
|
||||
self.my_climate.set_follow_underlying_temp_change(self._attr_is_on)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
self.turn_on()
|
||||
|
||||
@callback
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.turn_off()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
self._attr_is_on = False
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
self._attr_is_on = True
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
@@ -31,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,
|
||||
@@ -58,6 +62,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
|
||||
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
_is_auto_start_stop_enabled: bool = False
|
||||
_follow_underlying_temp_change: bool = False
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
@@ -78,6 +83,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"follow_underlying_temp_change",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -97,7 +103,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
|
||||
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
@@ -548,7 +554,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"auto_start_stop_accumulated_error_threshold"
|
||||
] = self._auto_start_stop_algo.accumulated_error_threshold
|
||||
|
||||
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
|
||||
self._follow_underlying_temp_change
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
@@ -595,8 +606,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",
|
||||
@@ -704,7 +725,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
)
|
||||
return
|
||||
|
||||
# Forget event when the new target temperature is out of range
|
||||
# Ignore new target temperature when out of range
|
||||
if (
|
||||
not new_target_temp is None
|
||||
and not self._attr_min_temp is None
|
||||
@@ -718,7 +739,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self._attr_min_temp,
|
||||
self._attr_max_temp,
|
||||
)
|
||||
return
|
||||
new_target_temp = None
|
||||
under_temp_diff = 0
|
||||
|
||||
# A real changes have to be managed
|
||||
_LOGGER.info(
|
||||
@@ -837,7 +859,12 @@ 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 (
|
||||
self._follow_underlying_temp_change
|
||||
and 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,
|
||||
@@ -956,6 +983,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self._is_auto_start_stop_enabled = is_enabled
|
||||
self.update_custom_attributes()
|
||||
|
||||
def set_follow_underlying_temp_change(self, follow: bool):
|
||||
"""Set the flaf follow the underlying temperature changes"""
|
||||
self._follow_underlying_temp_change = follow
|
||||
self.update_custom_attributes()
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self) -> str | None:
|
||||
"""Get the regulation mode"""
|
||||
@@ -1112,6 +1144,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""Returns the auto_start_stop_enable"""
|
||||
return self._is_auto_start_stop_enabled
|
||||
|
||||
@property
|
||||
def follow_underlying_temp_change(self) -> bool:
|
||||
"""Get the follow underlying temp change flag"""
|
||||
return self._follow_underlying_temp_change
|
||||
|
||||
@overrides
|
||||
def init_underlyings(self):
|
||||
"""Init the underlyings if not already done"""
|
||||
|
||||
@@ -21,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."""
|
||||
@@ -40,6 +42,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -82,6 +85,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
@@ -147,6 +151,9 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
self._attr_extra_state_attributes["function"] = self._proportional_function
|
||||
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
|
||||
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
|
||||
self._attr_extra_state_attributes[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -183,8 +190,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()
|
||||
|
||||
|
||||
@@ -25,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"""
|
||||
@@ -44,6 +46,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -97,6 +100,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
@@ -180,6 +184,9 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
self._attr_extra_state_attributes[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -265,8 +272,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()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
VTHERM_API_NAME = "vtherm_api"
|
||||
@@ -60,6 +61,7 @@ class VersatileThermostatAPI(dict):
|
||||
self._central_mode_select = None
|
||||
# A dict that will store all Number entities which holds the temperature
|
||||
self._number_temperatures = dict()
|
||||
self._max_on_percent = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
@@ -107,6 +109,12 @@ class VersatileThermostatAPI(dict):
|
||||
if self._safety_mode:
|
||||
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
|
||||
|
||||
self._max_on_percent = config.get(CONF_MAX_ON_PERCENT)
|
||||
if self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"We have found max_on_percent setting %s", self._max_on_percent
|
||||
)
|
||||
|
||||
def register_central_boiler(self, central_boiler_entity):
|
||||
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
|
||||
class to register itself at creation"""
|
||||
@@ -150,10 +158,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 +184,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"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,16 @@ 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,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.switch import (
|
||||
FollowUnderlyingTemperatureChange,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
@@ -195,7 +201,7 @@ async def test_bug_82(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_101(
|
||||
async def test_underlying_change_follow(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
@@ -229,12 +235,27 @@ async def test_bug_101(
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
assert entity.follow_underlying_temp_change is False
|
||||
|
||||
follow_entity: FollowUnderlyingTemperatureChange = search_entity(
|
||||
hass,
|
||||
"switch.theoverclimatemockname_follow_underlying_temp_change",
|
||||
SWITCH_DOMAIN,
|
||||
)
|
||||
assert follow_entity is not None
|
||||
assert follow_entity.state is STATE_OFF
|
||||
|
||||
# follow the underlying temp change
|
||||
follow_entity.turn_on()
|
||||
|
||||
assert entity.follow_underlying_temp_change is True
|
||||
assert follow_entity.state is STATE_ON
|
||||
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
@@ -320,6 +341,165 @@ 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_underlying_change_not_follow(
|
||||
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 change its own temperature target and switch to manual"""
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
assert entity.target_temperature == 15
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# default value
|
||||
assert entity.follow_underlying_temp_change is False
|
||||
|
||||
follow_entity: FollowUnderlyingTemperatureChange = search_entity(
|
||||
hass,
|
||||
"switch.theoverclimatemockname_follow_underlying_temp_change",
|
||||
SWITCH_DOMAIN,
|
||||
)
|
||||
assert follow_entity is not None
|
||||
assert follow_entity.state is STATE_OFF
|
||||
|
||||
# follow the underlying temp change
|
||||
follow_entity.turn_off()
|
||||
|
||||
assert entity.follow_underlying_temp_change is False
|
||||
assert follow_entity.state is STATE_OFF
|
||||
|
||||
# 1. Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at 11 sec later to avoid temporal filter
|
||||
event_timestamp = now + timedelta(seconds=30)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
21,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
# Should NOT have been switched to Manual preset
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
|
||||
@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,
|
||||
@@ -593,13 +773,26 @@ async def test_ignore_temp_outside_minmax_range(
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# 1. Force preset mode
|
||||
# 1. VTherm must follow the underlying's temperature changes
|
||||
follow_entity: FollowUnderlyingTemperatureChange = search_entity(
|
||||
hass,
|
||||
"switch.theoverclimatemockname_follow_underlying_temp_change",
|
||||
SWITCH_DOMAIN,
|
||||
)
|
||||
|
||||
# follow the underlying temp change
|
||||
follow_entity.turn_on()
|
||||
|
||||
assert entity.follow_underlying_temp_change is True
|
||||
assert follow_entity.state is STATE_ON
|
||||
|
||||
# 2. Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
|
||||
# 1. Try to set the target temperature to a below min_temp -> should be ignored
|
||||
# 3. Try to set the target temperature to a below min_temp -> should be ignored
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
@@ -607,8 +800,8 @@ async def test_ignore_temp_outside_minmax_range(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
entity.min_temp - 1,
|
||||
True,
|
||||
@@ -616,18 +809,393 @@ async def test_ignore_temp_outside_minmax_range(
|
||||
)
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
# 2. Try to set the target temperature to a above max_temp -> should be ignored
|
||||
# 4. Try to set the target temperature to a above max_temp -> should be ignored
|
||||
event_timestamp = event_timestamp + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
entity.max_temp + 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
# 5. Switch off the VTherm and receive an event from the underlying with a temp to be ignored,
|
||||
# but an HVACAction to be taken into account
|
||||
await entity.async_set_hvac_mode(HVACMode.OFF)
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
fake_underlying_climate.set_hvac_mode(HVACMode.OFF)
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.IDLE)
|
||||
|
||||
event_timestamp = event_timestamp + timedelta(seconds=11)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.IDLE,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
entity.min_temp - 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
@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
|
||||
|
||||
@@ -125,6 +125,39 @@ async def test_tpi_calculation(
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
"""
|
||||
Test the max_on_percent clamping calculations
|
||||
"""
|
||||
tpi_algo._max_on_percent = 0.8
|
||||
|
||||
# no clamping
|
||||
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 0.09
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
# no clamping (calculated_on_percent = 0.79)
|
||||
tpi_algo.calculate(15, 12.5, 11, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.79
|
||||
assert tpi_algo.calculated_on_percent == 0.79
|
||||
assert tpi_algo.on_time_sec == 237
|
||||
assert tpi_algo.off_time_sec == 63
|
||||
|
||||
# clamping to 80% (calculated_on_percent = 1)
|
||||
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.8 # should be clamped to 80%
|
||||
assert tpi_algo.calculated_on_percent == 1 # calculated percentage should not be affected by clamping
|
||||
assert tpi_algo.on_time_sec == 240 # capped at 80%
|
||||
assert tpi_algo.off_time_sec == 60
|
||||
|
||||
# clamping to 80% (calculated_on_percent = 0.81)
|
||||
tpi_algo.calculate(15, 12.5, 9, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.80 # should be clamped to 80%
|
||||
assert tpi_algo.calculated_on_percent == 0.81 # calculated percentage should not be affected by clamping
|
||||
assert tpi_algo.on_time_sec == 240 # capped at 80%
|
||||
assert tpi_algo.off_time_sec == 60
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
|
||||
Reference in New Issue
Block a user