Compare commits

..

24 Commits

Author SHA1 Message Date
Jean-Marc Collin
9dfdff924f Add #602 - implement a max_on_percent setting 2024-11-18 08:09:00 +00:00
Jean-Marc Collin
5e4cecdc37 Fix release name 2024-11-17 11:13:05 +00:00
Jean-Marc Collin
64968687f5 Release 2024-11-17 10:56:17 +00:00
Jean-Marc Collin
132c98a612 Fix Testus 2024-11-17 10:51:02 +00:00
Jean-Marc Collin
3113f3b57f Work in simuated environment 2024-11-17 10:39:59 +00:00
Jean-Marc Collin
f0526a0c93 With 1rst implementation of VTherm TRVZB and underlying 2024-11-17 07:47:44 +00:00
Jean-Marc Collin
c2677bf7c5 Next (not finished) 2024-11-16 21:43:53 +00:00
Jean-Marc Collin
8e01e69585 Fix configuration 2024-11-16 21:22:13 +00:00
Jean-Marc Collin
643f061fe2 With Sonoff configuration ok 2024-11-16 18:31:11 +00:00
Jean-Marc Collin
c1d1e8f1db Fix safety mode doc 2024-11-16 09:33:48 +00:00
Gernot Messow
71c35ecdc0 Fixed and extended unit test (#637) 2024-11-14 22:29:04 +01:00
Gernot Messow
4f8e45dda6 Just ignore illegal target temp, do not throw away all data (#635)
Co-authored-by: Gernot Messow <gmessow@insys-locks.de>
2024-11-14 21:54:15 +01:00
Jean-Marc Collin
d624c327b6 Issue #552 (#627)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-13 19:14:22 +01:00
Jean-Marc Collin
b46a24f834 Issue #628 add follow underlying temp change entity (#630)
* First commit (no test)

* With tests ok

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-13 19:14:03 +01:00
Jean-Marc Collin
d31376d55d Add Overkiz incompatilibity 2024-11-10 15:29:35 +00:00
Jean-Marc Collin
dbfd294ff3 Issue #496 - precision on safety parameters for over_climate 2024-11-10 10:03:33 +00:00
Jean-Marc Collin
e111bd0647 Removes most of the collapsible section in README. 2024-11-10 09:48:45 +00:00
Jean-Marc Collin
ba69319198 Issue #619 - manual hvac_off should be prioritized over window and auto-start/stop hvac_off (#622)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-10 10:17:53 +01:00
Jean-Marc Collin
f9df925181 Issue #615 - VTherm switch to manual on its own (#618)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-09 18:44:13 +01:00
Jean-Marc Collin
2d72efe447 Issue 600 energy can be negative after configuration (#614)
* Add logs to diagnose the case

* Issue #552 (#608)

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

* Fix typo (#607)

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

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2024-11-07 21:57:08 +01:00
Ludovic BOUÉ
95af6eba97 Fix typo (#607) 2024-11-05 22:47:42 +01:00
Jean-Marc Collin
06dc537767 Issue #552 (#608)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-05 22:39:26 +01:00
Joeri Colman
2d79d961dc Update en.json (#604)
fixed typo
2024-11-05 10:40:49 +01:00
Jean-Marc Collin
027bf8386b Add message into issue template. 2024-11-05 06:37:27 +00:00
27 changed files with 2197 additions and 587 deletions

View File

@@ -91,6 +91,48 @@ input_number:
icon: mdi:thermostat icon: mdi:thermostat
unit_of_measurement: °C unit_of_measurement: °C
mode: box mode: box
fake_offset_calibration1:
name: Sonoff offset calibration 1
min: -12
max: 12
icon: mdi:tune
unit_of_measurement: °C
mode: box
fake_opening_degree1:
name: Sonoff Opening degree 1
min: 0
max: 100
icon: mdi:valve-open
unit_of_measurement: "%"
mode: box
fake_closing_degree1:
name: Sonoff Closing degree 1
min: 0
max: 100
icon: mdi:valve-closed
unit_of_measurement: "%"
mode: box
fake_offset_calibration2:
name: Sonoff offset calibration 2
min: -12
max: 12
icon: mdi:tune
unit_of_measurement: °C
mode: box
fake_opening_degree2:
name: Sonoff Opening degree 2
min: 0
max: 100
icon: mdi:valve-open
unit_of_measurement: "%"
mode: box
fake_closing_degree2:
name: Sonoff Closing degree 2
min: 0
max: 100
icon: mdi:valve-closed
unit_of_measurement: "%"
mode: box
input_boolean: input_boolean:
# input_boolean to simulate the windows entity. Only for development environment. # input_boolean to simulate the windows entity. Only for development environment.
@@ -142,6 +184,12 @@ input_boolean:
fake_presence_sensor1: fake_presence_sensor1:
name: Presence Sensor 1 name: Presence Sensor 1
icon: mdi:home icon: mdi:home
fake_valve_sonoff_trvzb1:
name: Valve Sonoff TRVZB1
icon: mdi:valve
fake_valve_sonoff_trvzb2:
name: Valve Sonoff TRVZB2
icon: mdi:valve
climate: climate:
- platform: generic_thermostat - platform: generic_thermostat
@@ -152,6 +200,7 @@ climate:
name: Underlying thermostat2 name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat3 name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
@@ -184,6 +233,16 @@ climate:
name: Underlying thermostat9 name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying Sonoff TRVZB1
heater: input_boolean.fake_valve_sonoff_trvzb1
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying Sonoff TRVZB2
heater: input_boolean.fake_valve_sonoff_trvzb2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
input_datetime: input_datetime:
fake_last_seen: fake_last_seen:
@@ -237,14 +296,14 @@ switch:
friendly_name: "Pilote chauffage SDB RDC" friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}" value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on: turn_on:
service: select.select_option action: select.select_option
data: data:
option: comfort option: comfort
target: target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off: turn_off:
service: select.select_option action: select.select_option
data: data:
option: comfort-2 option: comfort-2
target: target:

View File

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

View File

@@ -13,6 +13,7 @@
- [Dans le cas d'une configuration centrale](#dans-le-cas-dune-configuration-centrale) - [Dans le cas d'une configuration centrale](#dans-le-cas-dune-configuration-centrale)
- [Refonte du menu de configuration](#refonte-du-menu-de-configuration) - [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) - [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) - [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) - [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Incompatibilités](#incompatibilités) - [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. > * **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>
<details>
<summary>Changements dans la version 6.0</summary>
# Changements dans la version 6.0 # Changements dans la version 6.0
## Entités de température pour les pre-réglages ## 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 : Cliquez sur cette option pour créér (resp. modifier) le VTherm :
![Configuration terminée](images/config-terminate.png) ![Configuration terminée](images/config-terminate.png)
</details>
<details> <details>
<summary>Changements dans la version 5.0</summary> <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 : 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", 1. Créer un VTherm de type "Configuration Centrale",
2. Saisir les attributs de cette 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. 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. 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. 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 ? # 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`. > 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 ## Création d'un nouveau Versatile Thermostat
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration 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. 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. 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 ## Choix des attributs de base
@@ -332,10 +332,6 @@ Donnez les principaux attributs obligatoires :
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](images/tips.png) _*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**, > 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. > 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) ## 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). 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. 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 ## 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). 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 ## 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). > 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 > 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 > 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 ## 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, > 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, > 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, ...) > 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 ## 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
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](images/tips.png) _*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 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 ## 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. > 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 > 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. > 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) ## Configurer la présence (ou l'absence)
@@ -696,10 +668,6 @@ ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](images/tips.png) _*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, > 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. > 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 ## 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. 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` : 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: 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, > 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``, > 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. > 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é ## 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 : Exemple de rendu :
![central_mode](images/central_mode.png) ![central_mode](images/central_mode.png)
</details>
<details>
<summary>Le contrôle d'une chaudière centrale</summary>
## Le contrôle d'une chaudière centrale ## Le contrôle d'une chaudière centrale
@@ -859,7 +821,6 @@ context:
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](images/tips.png) _*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. > 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> <details>
<summary>Synthèse des paramètres</summary> <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> <summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
## Pourquoi mon Versatile Thermostat se met en Securite ? ## 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). 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).

View File

@@ -13,7 +13,7 @@
- [In the case of a central configuration](#in-the-case-of-a-central-configuration) - [In the case of a central configuration](#in-the-case-of-a-central-configuration)
- [Redesign of the configuration menu](#redesign-of-the-configuration-menu) - [Redesign of the configuration menu](#redesign-of-the-configuration-menu)
- [The 'Incomplete configuration' and 'Finalize' menu options](#the-incomplete-configuration-and-finalize-menu-options) - [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) - [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use) - [When to use / not use](#when-to-use--not-use)
- [Incompatibilities](#incompatibilities) - [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. > * **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>
<details>
<summary>Changes in version 6.0</summary>
# Changes in version 6.0 # Changes in version 6.0
## Temperature entities for presets ## 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: Click on this option to create (resp. modify) the VTherm:
![Configuration Complete](images/config-terminate.png) ![Configuration Complete](images/config-terminate.png)
</details>
<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: 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”, 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. 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. 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. 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 ? # 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`. > 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 ## Creation of a new Versatile Thermostat
Click on Add integration button in the integration page 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: Then choose the type of VTherm you want to create:
![image](images/config-main0.png) ![image](images/config-main0.png)
</details>
<details>
<summary>Minimal configuration update</summary>
## Minimal configuration update ## Minimal configuration update
Then choose the “Main attributes” menu. Then choose the “Main attributes” menu.
@@ -330,10 +328,6 @@ Give the main mandatory attributes:
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](images/tips.png) _*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**, > 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. > 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 ## Select the driven entity
@@ -522,11 +516,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. 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 ## 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: Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the "TPI" menu option, you will get there:
@@ -534,11 +523,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). 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 ## 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 : 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 +543,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 > 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 > 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 ## 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. You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
@@ -605,11 +584,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, > 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, ...) > 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 ## 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: If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
![image](images/config-motion.png) ![image](images/config-motion.png)
@@ -634,11 +608,6 @@ For this to work, the climate thermostat should be in ``Activity`` preset mode.
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](images/tips.png) _*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 > 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 ## Configure the power management
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there: If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
@@ -656,10 +625,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. > 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 > 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. > 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 ## Configure presence or occupancy
@@ -682,11 +647,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, > 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. > 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 ## Advanced configuration
Those parameters allows to fine tune the thermostat. Those parameters allows to fine tune the thermostat.
@@ -703,6 +663,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. 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`: 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: versatile_thermostat:
@@ -721,11 +683,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``, > 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. > 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature.
</details>
<details>
<summary>Centralized control</summary>
## Centralized control ## 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. 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 +699,6 @@ Example rendering:
![central_mode](images/central_mode.png) ![central_mode](images/central_mode.png)
</details>
<details>
<summary>Control of a central boiler</summary>
## Control of a central boiler ## 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. 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,8 +800,6 @@ context:
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](images/tips.png) _*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. > 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>
<details> <details>
<summary>Parameter summary</summary> <summary>Parameter summary</summary>
@@ -1353,7 +1303,7 @@ Example of graph obtained with Plotly :
## And always better and better with the NOTIFIER daemon app to notify events ## 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). This is a great example of using the notifications described here [notification](#notifications).
@@ -1596,7 +1546,7 @@ These parameters are sensitive and quite difficult to adjust. Please only use th
## Why does my Versatile Thermostat go into Safety? ## 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). 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).

View File

@@ -178,13 +178,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if hass.state == CoreState.running: if hass.state == CoreState.running:
await api.reload_central_boiler_entities_list() await api.reload_central_boiler_entities_list()
await api.init_vtherm_links() await api.init_vtherm_links(entry.entry_id)
return True return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener.""" """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: if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
await reload_all_vtherm(hass) await reload_all_vtherm(hass)
else: else:
@@ -193,7 +200,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api is not None: if api is not None:
await api.reload_central_boiler_entities_list() 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -19,7 +19,10 @@ from homeassistant.core import (
) )
from homeassistant.components.climate import ClimateEntity 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.helpers.entity import Entity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
@@ -198,6 +201,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_translation_key = "versatile_thermostat" self._attr_translation_key = "versatile_thermostat"
self._total_energy = None self._total_energy = None
_LOGGER.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 # 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 self._underlying_climate_start_hvac_action_date = None
@@ -470,6 +474,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._presence_state = None self._presence_state = None
self._total_energy = None self._total_energy = None
_LOGGER.debug("%s - post_init_ resetting energy to None", self)
# Read the parameter from configuration.yaml if it exists # Read the parameter from configuration.yaml if it exists
short_ema_params = DEFAULT_SHORT_EMA_PARAMS short_ema_params = DEFAULT_SHORT_EMA_PARAMS
@@ -585,14 +590,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# issue 428. Link to others entities will start at link # issue 428. Link to others entities will start at link
# await self.async_startup() # await self.async_startup()
async def async_will_remove_from_hass(self):
"""Try to force backup of entity"""
_LOGGER.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): def remove_thermostat(self):
"""Called when the thermostat will be removed""" """Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self) _LOGGER.info("%s - Removing thermostat", self)
for under in self._underlyings: for under in self._underlyings:
under.remove_entity() under.remove_entity()
async def async_startup(self, central_configuration): 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", self)
_LOGGER.debug("%s - Calling async_startup_internal", self) _LOGGER.debug("%s - Calling async_startup_internal", self)
@@ -804,6 +819,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) 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 self._total_energy = old_total_energy if old_total_energy is not None else 0
_LOGGER.debug(
"%s - get_my_previous_state restored energy is %s",
self,
self._total_energy,
)
self.restore_specific_previous_state(old_state) self.restore_specific_previous_state(old_state)
else: else:
@@ -817,6 +837,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"No previously saved temperature, setting to %s", self._target_temp "No previously saved temperature, setting to %s", self._target_temp
) )
self._total_energy = 0 self._total_energy = 0
_LOGGER.debug(
"%s - get_my_previous_state no previous state energy is %s",
self,
self._total_energy,
)
if not self._hvac_mode: if not self._hvac_mode:
self._hvac_mode = HVACMode.OFF self._hvac_mode = HVACMode.OFF
@@ -1177,6 +1202,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if hvac_mode is None: if hvac_mode is None:
return 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 self._hvac_mode = hvac_mode
# Delegate to all underlying # Delegate to all underlying
@@ -1198,14 +1241,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Ensure we update the current operation after changing the mode # Ensure we update the current operation after changing the mode
self.reset_last_temperature_time() self.reset_last_temperature_time()
self.reset_last_change_time()
if self._hvac_mode != HVACMode.OFF: if self._hvac_mode != HVACMode.OFF:
self.set_hvac_off_reason(None) self.set_hvac_off_reason(None)
self.update_custom_attributes() save_state()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@overrides @overrides
async def async_set_preset_mode( async def async_set_preset_mode(
@@ -2198,8 +2238,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
save_all() save_all()
if new_central_mode == CENTRAL_MODE_STOPPED: if new_central_mode == CENTRAL_MODE_STOPPED:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL) if self.hvac_mode != HVACMode.OFF:
await self.async_set_hvac_mode(HVACMode.OFF) self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
return return
if new_central_mode == CENTRAL_MODE_COOL_ONLY: if new_central_mode == CENTRAL_MODE_COOL_ONLY:
@@ -2213,7 +2254,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if new_central_mode == CENTRAL_MODE_HEAT_ONLY: if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
if HVACMode.HEAT in self.hvac_modes: if HVACMode.HEAT in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.HEAT) 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) self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
return return
@@ -2622,6 +2664,22 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"hvac_off_reason": self.hvac_off_reason, "hvac_off_reason": self.hvac_off_reason,
} }
_LOGGER.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.debug(
"%s - async_write_ha_state written state energy is %s",
self,
self._total_energy,
)
return super().async_write_ha_state()
@callback @callback
def async_registry_entry_updated(self): def async_registry_entry_updated(self):
"""update the entity if the config entry have been updated """update the entity if the config entry have been updated

View File

@@ -37,11 +37,13 @@ from .const import (
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE, CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_SONOFF_TRZB_MODE,
) )
from .thermostat_switch import ThermostatOverSwitch from .thermostat_switch import ThermostatOverSwitch
from .thermostat_climate import ThermostatOverClimate from .thermostat_climate import ThermostatOverClimate
from .thermostat_valve import ThermostatOverValve from .thermostat_valve import ThermostatOverValve
from .thermostat_sonoff_trvzb import ThermostatOverSonoffTRVZB
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -60,6 +62,7 @@ async def async_setup_entry(
unique_id = entry.entry_id unique_id = entry.entry_id
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
is_sonoff_trvzb = entry.data.get(CONF_SONOFF_TRZB_MODE)
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
return return
@@ -69,7 +72,10 @@ async def async_setup_entry(
if vt_type == CONF_THERMOSTAT_SWITCH: if vt_type == CONF_THERMOSTAT_SWITCH:
entity = ThermostatOverSwitch(hass, unique_id, name, entry.data) entity = ThermostatOverSwitch(hass, unique_id, name, entry.data)
elif vt_type == CONF_THERMOSTAT_CLIMATE: elif vt_type == CONF_THERMOSTAT_CLIMATE:
entity = ThermostatOverClimate(hass, unique_id, name, entry.data) if is_sonoff_trvzb is True:
entity = ThermostatOverSonoffTRVZB(hass, unique_id, name, entry.data)
else:
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
elif vt_type == CONF_THERMOSTAT_VALVE: elif vt_type == CONF_THERMOSTAT_VALVE:
entity = ThermostatOverValve(hass, unique_id, name, entry.data) entity = ThermostatOverValve(hass, unique_id, name, entry.data)
else: else:

View File

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

View File

@@ -109,17 +109,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
) )
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get( 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) ) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get( self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG CONF_USE_POWER_CENTRAL_CONFIG, False
) or ( ) or (
self._infos.get(CONF_POWER_SENSOR) is not None self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
) )
self._infos[CONF_USE_PRESENCE_FEATURE] = ( 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 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[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 and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
) )
@@ -145,19 +145,43 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
): ):
if not is_empty: if not is_empty:
current_config = self._infos.get(config, None) 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: else:
self._infos[config] = self._central_config is not None self._infos[config] = self._central_config is not None
if COMES_FROM in self._infos: if COMES_FROM in self._infos:
del self._infos[COMES_FROM] del self._infos[COMES_FROM]
async def validate_input(self, data: dict) -> None: def check_sonoff_trvzb_nb_entities(self, data: dict) -> bool:
"""Check the number of entities for Sonoff TRVZB"""
ret = True
if (
self._infos.get(CONF_SONOFF_TRZB_MODE)
and data.get(CONF_OFFSET_CALIBRATION_LIST) is not None
):
nb_unders = len(self._infos.get(CONF_UNDERLYING_LIST))
nb_offset = len(data.get(CONF_OFFSET_CALIBRATION_LIST))
nb_opening = len(data.get(CONF_OPENING_DEGREE_LIST))
nb_closing = len(data.get(CONF_CLOSING_DEGREE_LIST))
if (
nb_unders != nb_offset
or nb_unders != nb_opening
or nb_unders != nb_closing
):
ret = False
return ret
async def validate_input(self, data: dict, step_id) -> None:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user. Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
@@ -173,6 +197,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR, CONF_MAX_POWER_SENSOR,
CONF_PRESENCE_SENSOR, CONF_PRESENCE_SENSOR,
CONF_OFFSET_CALIBRATION_LIST,
CONF_OPENING_DEGREE_LIST,
CONF_CLOSING_DEGREE_LIST,
]: ]:
d = data.get(conf, None) # pylint: disable=invalid-name d = data.get(conf, None) # pylint: disable=invalid-name
if not isinstance(d, list): if not isinstance(d, list):
@@ -209,6 +236,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_ADVANCED_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: if data.get(conf) is True:
_LOGGER.error( _LOGGER.error(
@@ -227,6 +257,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
except ServiceConfigurationError as err: except ServiceConfigurationError as err:
raise ServiceConfigurationError(conf) from err raise ServiceConfigurationError(conf) from err
# Check that the number of offet_calibration and opening_degree and closing_degree are equals
# to the number of underlying entities
if not self.check_sonoff_trvzb_nb_entities(data):
raise SonoffTRVZBNbEntitiesIncorrect()
def check_config_complete(self, infos) -> bool: def check_config_complete(self, infos) -> bool:
"""True if the config is now complete (ie all mandatory attributes are set)""" """True if the config is now complete (ie all mandatory attributes are set)"""
is_central_config = ( is_central_config = (
@@ -306,6 +341,25 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
): ):
return False 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
if not self.check_sonoff_trvzb_nb_entities(infos):
return False
return True return True
def merge_user_input(self, data_schema: vol.Schema, user_input: dict): def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
@@ -335,7 +389,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
if user_input is not None: if user_input is not None:
defaults.update(user_input or {}) defaults.update(user_input or {})
try: try:
await self.validate_input(user_input) await self.validate_input(user_input, step_id)
except UnknownEntity as err: except UnknownEntity as err:
errors[str(err)] = "unknown_entity" errors[str(err)] = "unknown_entity"
except WindowOpenDetectionMethod as err: except WindowOpenDetectionMethod as err:
@@ -346,6 +400,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
errors[str(err)] = "service_configuration_format" errors[str(err)] = "service_configuration_format"
except ConfigurationNotCompleteError as err: except ConfigurationNotCompleteError as err:
errors["base"] = "configuration_not_complete" errors["base"] = "configuration_not_complete"
except SonoffTRVZBNbEntitiesIncorrect as err:
errors["base"] = "sonoff_trvzb_nb_entities_incorrect"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -432,6 +488,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
]: ]:
menu_options.append("auto_start_stop") menu_options.append("auto_start_stop")
if self._infos.get(CONF_SONOFF_TRZB_MODE) is True:
menu_options.append("sonoff_trvzb")
menu_options.append("advanced") menu_options.append("advanced")
if self.check_config_complete(self._infos): if self.check_config_complete(self._infos):
@@ -501,6 +560,24 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the Type flow steps""" """Handle the Type flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_type user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_type user_input=%s", user_input)
if (
self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE
and user_input is not None
and not user_input.get(CONF_SONOFF_TRZB_MODE)
):
# Remove TPI info
for key in [
PROPORTIONAL_FUNCTION_TPI,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT,
CONF_OFFSET_CALIBRATION_LIST,
CONF_OPENING_DEGREE_LIST,
CONF_CLOSING_DEGREE_LIST,
]:
if self._infos.get(key):
del self._infos[key]
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
return await self.generic_step( return await self.generic_step(
"type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_menu "type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_menu
@@ -544,6 +621,20 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
return await self.generic_step("auto_start_stop", schema, user_input, next_step) return await self.generic_step("auto_start_stop", schema, user_input, next_step)
async def async_step_sonoff_trvzb(
self, user_input: dict | None = None
) -> FlowResult:
"""Handle the Sonoff TRVZB configuration step"""
_LOGGER.debug(
"Into ConfigFlow.async_step_sonoff_trvzb user_input=%s", user_input
)
schema = STEP_SONOFF_TRVZB
self._infos[COMES_FROM] = None
next_step = self.async_step_menu
return await self.generic_step("sonoff_trvzb", schema, user_input, next_step)
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult: async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
"""Handle the TPI flow steps""" """Handle the TPI flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)

View File

@@ -141,6 +141,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True), selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean, vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_SONOFF_TRZB_MODE, default=False): cv.boolean,
vol.Optional( vol.Optional(
CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE
): selector.SelectSelector( ): selector.SelectSelector(
@@ -197,6 +198,31 @@ STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
} }
) )
STEP_SONOFF_TRVZB = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_OFFSET_CALIBRATION_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_OPENING_DEGREE_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_CLOSING_DEGREE_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[
PROPORTIONAL_FUNCTION_TPI,
]
),
}
)
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean, vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,

View File

@@ -94,6 +94,7 @@ CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature" CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature" CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
CONF_AC_MODE = "ac_mode" CONF_AC_MODE = "ac_mode"
CONF_SONOFF_TRZB_MODE = "sonoff_trvzb_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold" CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold" CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration" CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
@@ -115,6 +116,9 @@ CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
CONF_AUTO_FAN_HIGH = "auto_fan_high" CONF_AUTO_FAN_HIGH = "auto_fan_high"
CONF_AUTO_FAN_TURBO = "auto_fan_turbo" CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
CONF_STEP_TEMPERATURE = "step_temperature" CONF_STEP_TEMPERATURE = "step_temperature"
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
# Deprecated # Deprecated
CONF_HEATER = "heater_entity_id" CONF_HEATER = "heater_entity_id"
@@ -287,6 +291,7 @@ ALL_CONF = (
CONF_USE_POWER_FEATURE, CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE, CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_AC_MODE, CONF_AC_MODE,
CONF_SONOFF_TRZB_MODE,
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN, CONF_AUTO_REGULATION_PERIOD_MIN,
@@ -354,7 +359,11 @@ CONF_WINDOW_ACTIONS = [
CONF_WINDOW_ECO_TEMP, 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_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
@@ -501,6 +510,11 @@ class ConfigurationNotCompleteError(HomeAssistantError):
"""Error the configuration is not complete""" """Error the configuration is not complete"""
class SonoffTRVZBNbEntitiesIncorrect(HomeAssistantError):
"""Error to indicate there is an error in the configuration of the Sonoff TRVZB.
The number of specific entities is incorrect."""
class overrides: # pylint: disable=invalid-name class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides""" """An annotation to inform overrides"""

View File

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

View File

@@ -17,7 +17,6 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorDeviceClass, SensorDeviceClass,
SensorStateClass, SensorStateClass,
UnitOfTemperature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -50,6 +49,7 @@ from .const import (
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_USE_CENTRAL_BOILER_FEATURE, CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_SONOFF_TRZB_MODE,
overrides, overrides,
) )
@@ -71,6 +71,7 @@ async def async_setup_entry(
unique_id = entry.entry_id unique_id = entry.entry_id
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
is_sonoff_trvzb = entry.data.get(CONF_SONOFF_TRZB_MODE)
entities = None entities = None
@@ -99,10 +100,16 @@ async def async_setup_entry(
entities.append(OnTimeSensor(hass, unique_id, name, entry.data)) entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
entities.append(OffTimeSensor(hass, unique_id, name, entry.data)) entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE: if (
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
or is_sonoff_trvzb
):
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data)) entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE: if (
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
and not is_sonoff_trvzb
):
entities.append( entities.append(
RegulatedTemperatureSensor(hass, unique_id, name, entry.data) RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
) )

View File

@@ -28,6 +28,7 @@
"presence": "Presence detection", "presence": "Presence detection",
"advanced": "Advanced parameters", "advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop", "auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done", "finalize": "All done",
"configuration_not_complete": "Configuration not complete" "configuration_not_complete": "Configuration not complete"
} }
@@ -76,6 +77,7 @@
"heater_keep_alive": "Switch keep-alive interval in seconds", "heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"ac_mode": "AC mode", "ac_mode": "AC mode",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
@@ -88,6 +90,7 @@
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.", "heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"ac_mode": "Use the Air Conditioning (AC) mode", "ac_mode": "Use the Air Conditioning (AC) mode",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -203,6 +206,34 @@
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
},
"central_boiler": {
"title": "Control of the central boiler",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
} }
}, },
"error": { "error": {
@@ -243,6 +274,7 @@
"presence": "Presence detection", "presence": "Presence detection",
"advanced": "Advanced parameters", "advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop", "auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done", "finalize": "All done",
"configuration_not_complete": "Configuration not complete" "configuration_not_complete": "Configuration not complete"
} }
@@ -291,6 +323,7 @@
"heater_keep_alive": "Switch keep-alive interval in seconds", "heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"ac_mode": "AC mode", "ac_mode": "AC mode",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
@@ -303,6 +336,7 @@
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.", "heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"ac_mode": "Use the Air Conditioning (AC) mode", "ac_mode": "Use the Air Conditioning (AC) mode",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -418,6 +452,34 @@
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
},
"central_boiler": {
"title": "Control of the central boiler - {name}",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration - {name}",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
} }
}, },
"error": { "error": {
@@ -425,7 +487,8 @@
"unknown_entity": "Unknown entity id", "unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both", "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.", "no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong" "service_configuration_format": "The format of the service configuration is wrong",
"sonoff_trvzb_nb_entities_incorrect": "The number of specific entities for Sonoff TRVZB should be equal to the number of underlyings"
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"

View File

@@ -34,10 +34,16 @@ async def async_setup_entry(
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE) 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: entities = []
# Creates a switch to enable the auto-start/stop if vt_type == CONF_THERMOSTAT_CLIMATE:
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry) entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
async_add_entities([enable_entity], True)
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): class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
@@ -100,3 +106,63 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
def turn_on(self, **kwargs: Any): def turn_on(self, **kwargs: Any):
self._attr_is_on = True self._attr_is_on = True
self.update_my_state_and_vtherm() 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()

View File

@@ -1,5 +1,5 @@
# pylint: disable=line-too-long, too-many-lines # pylint: disable=line-too-long, too-many-lines, abstract-method
""" A climate over switch classe """ """ A climate over climate classe """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
@@ -58,28 +58,28 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE _auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None _auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
_is_auto_start_stop_enabled: bool = False _is_auto_start_stop_enabled: bool = False
_follow_underlying_temp_change: bool = False
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
BaseThermostat._entity_component_unrecorded_attributes.union( frozenset(
frozenset( {
{ "is_over_climate",
"is_over_climate", "start_hvac_action_date",
"start_hvac_action_date", "underlying_entities",
"underlying_entities", "regulation_accumulated_error",
"regulation_accumulated_error", "auto_regulation_mode",
"auto_regulation_mode", "auto_fan_mode",
"auto_fan_mode", "current_auto_fan_mode",
"current_auto_fan_mode", "auto_activated_fan_mode",
"auto_activated_fan_mode", "auto_deactivated_fan_mode",
"auto_deactivated_fan_mode", "auto_regulation_use_device_temp",
"auto_regulation_use_device_temp", "auto_start_stop_level",
"auto_start_stop_level", "auto_start_stop_dtmin",
"auto_start_stop_dtmin", "auto_start_stop_enable",
"auto_start_stop_enable", "auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error", "auto_start_stop_accumulated_error_threshold",
"auto_start_stop_accumulated_error_threshold", "follow_underlying_temp_change",
} }
)
) )
) )
@@ -97,15 +97,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(config_entry)
for climate in config_entry.get(CONF_UNDERLYING_LIST): for climate in config_entry.get(CONF_UNDERLYING_LIST):
self._underlyings.append( under = UnderlyingClimate(
UnderlyingClimate( hass=self._hass,
hass=self._hass, thermostat=self,
thermostat=self, climate_entity_id=climate,
climate_entity_id=climate,
)
) )
self._underlyings.append(under)
self.choose_auto_regulation_mode( self.choose_auto_regulation_mode(
config_entry.get(CONF_AUTO_REGULATION_MODE) config_entry.get(CONF_AUTO_REGULATION_MODE)
@@ -548,7 +547,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_start_stop_accumulated_error_threshold" "auto_start_stop_accumulated_error_threshold"
] = self._auto_start_stop_algo.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() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(
"%s - Calling update_custom_attributes: %s", "%s - Calling update_custom_attributes: %s",
self, self,
@@ -595,8 +599,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
if self._total_energy is None: if self._total_energy is None:
self._total_energy = added_energy self._total_energy = added_energy
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else: else:
self._total_energy += added_energy self._total_energy += added_energy
_LOGGER.debug(
"%s - incremente_energy incremented energy is %s",
self,
self._total_energy,
)
_LOGGER.debug( _LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f", "%s - added energy is %.3f . Total energy is now: %.3f",
@@ -704,7 +718,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
) )
return return
# Forget event when the new target temperature is out of range # Ignore new target temperature when out of range
if ( if (
not new_target_temp is None not new_target_temp is None
and not self._attr_min_temp is None and not self._attr_min_temp is None
@@ -718,7 +732,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
self._attr_min_temp, self._attr_min_temp,
self._attr_max_temp, self._attr_max_temp,
) )
return new_target_temp = None
under_temp_diff = 0
# A real changes have to be managed # A real changes have to be managed
_LOGGER.info( _LOGGER.info(
@@ -837,7 +852,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
changes = True changes = True
# try to manage new target temperature set if state if no other changes have been found # 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( _LOGGER.debug(
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s", "Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
under.last_sent_temperature, under.last_sent_temperature,
@@ -901,7 +921,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# Stop here # Stop here
return False return False
elif action == AUTO_START_STOP_ACTION_ON: elif (
action == AUTO_START_STOP_ACTION_ON
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.info( _LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self "%s - Turning ON the Vtherm due to auto-start-stop conditions", self
) )
@@ -939,7 +962,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
if not continu: if not continu:
return ret return ret
else: else:
_LOGGER.debug("%s - auto start/stop is disabled") _LOGGER.debug("%s - auto start/stop is disabled", self)
# Continue the normal async_control_heating # Continue the normal async_control_heating
@@ -956,6 +979,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
self._is_auto_start_stop_enabled = is_enabled self._is_auto_start_stop_enabled = is_enabled
self.update_custom_attributes() 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 @property
def auto_regulation_mode(self) -> str | None: def auto_regulation_mode(self) -> str | None:
"""Get the regulation mode""" """Get the regulation mode"""
@@ -1083,6 +1111,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return None return None
@property
def current_humidity(self) -> float | None:
"""Return the humidity."""
if self.underlying_entity(0):
return self.underlying_entity(0).current_humidity
return None
@property @property
def is_aux_heat(self) -> bool | None: def is_aux_heat(self) -> bool | None:
"""Return true if aux heater. """Return true if aux heater.
@@ -1112,6 +1148,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Returns the auto_start_stop_enable""" """Returns the auto_start_stop_enable"""
return self._is_auto_start_stop_enabled 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 @overrides
def init_underlyings(self): def init_underlyings(self):
"""Init the underlyings if not already done""" """Init the underlyings if not already done"""

View File

@@ -0,0 +1,242 @@
# pylint: disable=line-too-long, too-many-lines, abstract-method
""" A climate over Sonoff TRVZB classe """
import logging
from datetime import datetime
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode
from .underlyings import UnderlyingSonoffTRVZB
# from .commons import NowClass, round_to_nearest
from .base_thermostat import ConfigData
from .thermostat_climate import ThermostatOverClimate
from .prop_algorithm import PropAlgorithm
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
"""This class represent a VTherm over a Sonoff TRVZB climate"""
_entity_component_unrecorded_attributes = ThermostatOverClimate._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_climate",
"is_over_sonoff_trvzb",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
}
)
)
_underlyings_sonoff_trvzb: list[UnderlyingSonoffTRVZB] = []
_valve_open_percent: int = 0
_last_calculation_timestamp: datetime | None = None
_auto_regulation_dpercent: float | None = None
_auto_regulation_period_min: int | None = None
def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
):
"""Initialize the ThermostatOverSonoffTRVZB class"""
_LOGGER.debug("%s - creating a ThermostatOverSonoffTRVZB VTherm", name)
super().__init__(hass, unique_id, name, entry_infos)
# self._valve_open_percent: int = 0
# self._last_calculation_timestamp: datetime | None = None
# self._auto_regulation_dpercent: float | None = None
# self._auto_regulation_period_min: int | None = None
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat and underlyings
Beware that the underlyings list contains the climate which represent the Sonoff TRVZB
but also the UnderlyingSonoff which reprensent the valve"""
super().post_init(config_entry)
self._auto_regulation_dpercent = (
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.0
)
self._auto_regulation_period_min = (
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 0
)
# Initialization of the TPI algo
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
self.name,
)
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
offset = config_entry.get(CONF_OFFSET_CALIBRATION_LIST)[idx]
opening = config_entry.get(CONF_OPENING_DEGREE_LIST)[idx]
closing = config_entry.get(CONF_CLOSING_DEGREE_LIST)[idx]
under = UnderlyingSonoffTRVZB(
hass=self._hass,
thermostat=self,
offset_calibration_entity_id=offset,
opening_degree_entity_id=opening,
closing_degree_entity_id=closing,
)
self._underlyings_sonoff_trvzb.append(under)
@overrides
def update_custom_attributes(self):
"""Custom attributes"""
super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_sonoff_trvzb"] = (
self.is_over_sonoff_trvzb
)
self._attr_extra_state_attributes["underlying_sonoff_trvzb_entities"] = [
underlying.entity_id for underlying in self._underlyings_sonoff_trvzb
]
self._attr_extra_state_attributes["on_percent"] = (
self._prop_algorithm.on_percent
)
self._attr_extra_state_attributes["power_percent"] = self.power_percent
self._attr_extra_state_attributes["on_time_sec"] = (
self._prop_algorithm.on_time_sec
)
self._attr_extra_state_attributes["off_time_sec"] = (
self._prop_algorithm.off_time_sec
)
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
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["valve_open_percent"] = (
self.valve_open_percent
)
self._attr_extra_state_attributes["auto_regulation_dpercent"] = (
self._auto_regulation_dpercent
)
self._attr_extra_state_attributes["auto_regulation_period_min"] = (
self._auto_regulation_period_min
)
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
if self._last_calculation_timestamp
else None
)
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@overrides
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate the open percent", self)
# TODO this is exactly the same method as the thermostat_valve recalculate. Put that in common
# For testing purpose. Should call _set_now() before
now = self.now
if self._last_calculation_timestamp is not None:
period = (now - self._last_calculation_timestamp).total_seconds() / 60
if period < self._auto_regulation_period_min:
_LOGGER.info(
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
self,
period,
)
return
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF,
)
new_valve_percent = round(
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
)
# Issue 533 - don't filter with dtemp if valve should be close. Else it will never close
if new_valve_percent < self._auto_regulation_dpercent:
new_valve_percent = 0
dpercent = new_valve_percent - self.valve_open_percent
if (
new_valve_percent > 0
and -1 * self._auto_regulation_dpercent
<= dpercent
< self._auto_regulation_dpercent
):
_LOGGER.debug(
"%s - do not calculate TPI because regulation_dpercent (%.1f) is not exceeded",
self,
dpercent,
)
return
if self._valve_open_percent == new_valve_percent:
_LOGGER.debug("%s - no change in valve_open_percent.", self)
return
self._valve_open_percent = new_valve_percent
for under in self._underlyings_sonoff_trvzb:
under.set_valve_open_percent()
self._last_calculation_timestamp = now
self.update_custom_attributes()
@property
def is_over_sonoff_trvzb(self) -> bool:
"""True if the Thermostat is over_sonoff_trvzb"""
return True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
# @property
# def hvac_modes(self) -> list[HVACMode]:
# """Get the hvac_modes"""
# return self._hvac_list
@property
def valve_open_percent(self) -> int:
"""Gives the percentage of valve needed"""
if self._hvac_mode == HVACMode.OFF:
return 0
else:
return self._valve_open_percent

View File

@@ -1,4 +1,4 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long, abstract-method
""" A climate over switch classe """ """ A climate over switch classe """
import logging import logging
@@ -22,26 +22,23 @@ from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]): class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch.""" """Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
BaseThermostat._entity_component_unrecorded_attributes.union( frozenset(
frozenset( {
{ "is_over_switch",
"is_over_switch", "is_inversed",
"is_inversed", "underlying_entities",
"underlying_entities", "on_time_sec",
"on_time_sec", "off_time_sec",
"off_time_sec", "cycle_min",
"cycle_min", "function",
"function", "tpi_coef_int",
"tpi_coef_int", "tpi_coef_ext",
"tpi_coef_ext", "power_percent",
"power_percent", }
}
)
) )
) )
@@ -183,8 +180,18 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
if self._total_energy is None: if self._total_energy is None:
self._total_energy = added_energy self._total_energy = added_energy
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else: else:
self._total_energy += added_energy self._total_energy += added_energy
_LOGGER.debug(
"%s - incremente_energy increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes() self.update_custom_attributes()

View File

@@ -1,4 +1,4 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long, abstract-method
""" A climate over switch classe """ """ A climate over switch classe """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
@@ -26,7 +26,6 @@ from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
"""Representation of a class for a Versatile Thermostat over a Valve""" """Representation of a class for a Versatile Thermostat over a Valve"""
@@ -265,8 +264,18 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
if self._total_energy is None: if self._total_energy is None:
self._total_energy = added_energy self._total_energy = added_energy
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else: else:
self._total_energy += added_energy self._total_energy += added_energy
_LOGGER.debug(
"%s - get_my_previous_state increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes() self.update_custom_attributes()

View File

@@ -28,6 +28,7 @@
"presence": "Presence detection", "presence": "Presence detection",
"advanced": "Advanced parameters", "advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop", "auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done", "finalize": "All done",
"configuration_not_complete": "Configuration not complete" "configuration_not_complete": "Configuration not complete"
} }
@@ -76,6 +77,7 @@
"heater_keep_alive": "Switch keep-alive interval in seconds", "heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"ac_mode": "AC mode", "ac_mode": "AC mode",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
@@ -88,6 +90,7 @@
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.", "heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"ac_mode": "Use the Air Conditioning (AC) mode", "ac_mode": "Use the Air Conditioning (AC) mode",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -203,6 +206,34 @@
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
},
"central_boiler": {
"title": "Control of the central boiler",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
} }
}, },
"error": { "error": {
@@ -243,6 +274,7 @@
"presence": "Presence detection", "presence": "Presence detection",
"advanced": "Advanced parameters", "advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop", "auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done", "finalize": "All done",
"configuration_not_complete": "Configuration not complete" "configuration_not_complete": "Configuration not complete"
} }
@@ -291,6 +323,7 @@
"heater_keep_alive": "Switch keep-alive interval in seconds", "heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"ac_mode": "AC mode", "ac_mode": "AC mode",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
@@ -303,6 +336,7 @@
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.", "heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"ac_mode": "Use the Air Conditioning (AC) mode", "ac_mode": "Use the Air Conditioning (AC) mode",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
@@ -418,6 +452,34 @@
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
},
"central_boiler": {
"title": "Control of the central boiler",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
} }
}, },
"error": { "error": {
@@ -425,7 +487,8 @@
"unknown_entity": "Unknown entity id", "unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both", "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.", "no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong" "service_configuration_format": "The format of the service configuration is wrong",
"sonoff_trvzb_nb_entities_incorrect": "The number of specific entities for Sonoff TRVZB should be equal to the number of underlyings"
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"

View File

@@ -28,6 +28,7 @@
"presence": "Détection de présence", "presence": "Détection de présence",
"advanced": "Paramètres avancés", "advanced": "Paramètres avancés",
"auto_start_stop": "Allumage/extinction automatique", "auto_start_stop": "Allumage/extinction automatique",
"sonoff_trvzb": "Configuration spécifique à Sonoff TRVZB",
"finalize": "Finaliser la création", "finalize": "Finaliser la création",
"configuration_not_complete": "Configuration incomplète" "configuration_not_complete": "Configuration incomplète"
} }
@@ -72,48 +73,28 @@
"title": "Entité(s) liée(s)", "title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)", "description": "Attributs de(s) l'entité(s) liée(s)",
"data": { "data": {
"heater_entity_id": "1er radiateur", "underlying_entity_ids": "Les équipements à controller",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"heater_keep_alive": "keep-alive (sec)", "heater_keep_alive": "keep-alive (sec)",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?", "ac_mode": "AC mode ?",
"valve_entity_id": "1ère valve number", "sonoff_trvzb_mode": "Mode Sonoff TRVZB",
"valve_entity2_id": "2ème valve number",
"valve_entity3_id": "3ème valve number",
"valve_entity4_id": "4ème valve number",
"auto_regulation_mode": "Auto-régulation", "auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation", "auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent", "auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande", "inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto ventilation mode" "auto_fan_mode": " Auto ventilation mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.", "heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)", "ac_mode": "Utilisation du mode Air Conditionné (AC)",
"valve_entity_id": "Entity id de la 1ère valve", "sonoff_trvzb_mode": "Les équipements sont des Sonoff TRVZB. Vous devez configurer les entités dédiées dans le menu 'Configuration Sonoff TRVZB'",
"valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_mode": "Ajustement automatique de la température cible",
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation", "auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important" "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
} }
@@ -237,6 +218,22 @@
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]", "central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]" "central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
} }
},
"sonoff_trvzb": {
"title": "Configuration Sonoff TRVZB",
"description": "Configuration spécifique des Sonoff TRVZB",
"data": {
"offset_calibration_entity_ids": "Entités de 'Offset calibration'",
"opening_degree_entity_ids": "Entités de 'Opening degree'",
"closing_degree_entity_ids": "Entités de 'Closing degree'",
"proportional_function": "Algorithme"
},
"data_description": {
"offset_calibration_entity_ids": "La liste des entités 'offset calibration' entities. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'opening degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'closing degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
}
} }
}, },
"error": { "error": {
@@ -262,7 +259,7 @@
} }
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu - {name}",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.", "description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": { "menu_options": {
"main": "Principaux Attributs", "main": "Principaux Attributs",
@@ -277,6 +274,7 @@
"presence": "Détection de présence", "presence": "Détection de présence",
"advanced": "Paramètres avancés", "advanced": "Paramètres avancés",
"auto_start_stop": "Allumage/extinction automatique", "auto_start_stop": "Allumage/extinction automatique",
"sonoff_trvzb": "Configuration spécifique à Sonoff TRVZB",
"finalize": "Finaliser les modifications", "finalize": "Finaliser les modifications",
"configuration_not_complete": "Configuration incomplète" "configuration_not_complete": "Configuration incomplète"
} }
@@ -318,51 +316,31 @@
} }
}, },
"type": { "type": {
"title": "Entités - {name}", "title": "Entité(s) liée(s) - {name}",
"description": "Attributs de(s) l'entité(s) liée(s)", "description": "Attributs de(s) l'entité(s) liée(s)",
"data": { "data": {
"heater_entity_id": "1er radiateur", "underlying_entity_ids": "Les équipements à controller",
"heater_entity2_id": "2ème radiateur", "heater_keep_alive": "keep-alive (sec)",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"heater_keep_alive": "Keep-alive (sec)",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?", "ac_mode": "AC mode ?",
"valve_entity_id": "1ère valve", "sonoff_trvzb_mode": "Mode Sonoff TRVZB",
"valve_entity2_id": "2ème valve", "auto_regulation_mode": "Auto-régulation",
"valve_entity3_id": "3ème valve",
"valve_entity4_id": "4ème valve",
"auto_regulation_mode": "Auto-regulation",
"auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation", "auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent", "auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande", "inverse_switch_command": "Inverser la commande",
"auto_fan_mode": "Auto fan mode" "auto_fan_mode": " Auto ventilation mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.", "heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)", "ac_mode": "Utilisation du mode Air Conditionné (AC)",
"valve_entity_id": "Entity id de la 1ère valve", "sonoff_trvzb_mode": "Les équipements sont des Sonoff TRVZB. Vous devez configurer les entités dédiées dans le menu 'Configuration Sonoff TRVZB'",
"valve_entity2_id": "Entity id de la 2ème valve", "auto_regulation_mode": "Ajustement automatique de la température cible",
"valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la consigne",
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation", "auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important" "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
} }
@@ -480,6 +458,22 @@
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]", "central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]" "central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
} }
},
"sonoff_trvzb": {
"title": "Configuration Sonoff TRVZB - {name}",
"description": "Configuration spécifique des Sonoff TRVZB",
"data": {
"offset_calibration_entity_ids": "Entités de 'Offset calibration'",
"opening_degree_entity_ids": "Entités de 'Opening degree'",
"closing_degree_entity_ids": "Entités de 'Closing degree'",
"proportional_function": "Algorithme"
},
"data_description": {
"offset_calibration_entity_ids": "La liste des entités 'offset calibration' entities. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'opening degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'closing degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
}
} }
}, },
"error": { "error": {
@@ -487,7 +481,8 @@
"unknown_entity": "entity id inconnu", "unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.", "window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.", "no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
"service_configuration_format": "Mauvais format de la configuration du service" "service_configuration_format": "Mauvais format de la configuration du service",
"sonoff_trvzb_nb_entities_incorrect": "Le nombre d'entités spécifiques au Sonoff TRVZB doit être égal au nombre d'entité sous-jacentes"
}, },
"abort": { "abort": {
"already_configured": "Le device est déjà configuré" "already_configured": "Le device est déjà configuré"

View File

@@ -1,4 +1,4 @@
# pylint: disable=unused-argument, line-too-long # pylint: disable=unused-argument, line-too-long, too-many-lines
""" Underlying entities classes """ """ Underlying entities classes """
import logging import logging
@@ -53,6 +53,9 @@ class UnderlyingEntityType(StrEnum):
# a valve # a valve
VALVE = "valve" VALVE = "valve"
# a Sonoff TRVZB
SONOFF_TRVZB = "sonoff_trvzb"
class UnderlyingEntity: class UnderlyingEntity:
"""Represent a underlying device which could be a switch or a climate""" """Represent a underlying device which could be a switch or a climate"""
@@ -62,6 +65,7 @@ class UnderlyingEntity:
_thermostat: Any _thermostat: Any
_entity_id: str _entity_id: str
_type: UnderlyingEntityType _type: UnderlyingEntityType
_hvac_mode: HVACMode | None
def __init__( def __init__(
self, self,
@@ -75,6 +79,7 @@ class UnderlyingEntity:
self._thermostat = thermostat self._thermostat = thermostat
self._type = entity_type self._type = entity_type
self._entity_id = entity_id self._entity_id = entity_id
self._hvac_mode = None
def __str__(self): def __str__(self):
return str(self._thermostat) + "-" + self._entity_id return str(self._thermostat) + "-" + self._entity_id
@@ -100,13 +105,24 @@ class UnderlyingEntity:
async def set_hvac_mode(self, hvac_mode: HVACMode): async def set_hvac_mode(self, hvac_mode: HVACMode):
"""Set the HVACmode""" """Set the HVACmode"""
self._hvac_mode = hvac_mode
return return
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current hvac_mode"""
return self._hvac_mode
@property @property
def is_device_active(self) -> bool | None: def is_device_active(self) -> bool | None:
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
return None return None
@property
def hvac_action(self) -> HVACAction:
"""Calculate a hvac_action"""
return HVACAction.HEATING if self.is_device_active is True else HVACAction.OFF
async def set_temperature(self, temperature, max_temp, min_temp): async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature""" """Set the target temperature"""
return return
@@ -181,7 +197,6 @@ class UnderlyingSwitch(UnderlyingEntity):
_initialDelaySec: int _initialDelaySec: int
_on_time_sec: int _on_time_sec: int
_off_time_sec: int _off_time_sec: int
_hvac_mode: HVACMode
def __init__( def __init__(
self, self,
@@ -204,7 +219,6 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = 0 self._off_time_sec = 0
self._hvac_mode = None
self._keep_alive = IntervalCaller(hass, keep_alive_sec) self._keep_alive = IntervalCaller(hass, keep_alive_sec)
@property @property
@@ -237,8 +251,8 @@ class UnderlyingSwitch(UnderlyingEntity):
await self.turn_off() await self.turn_off()
self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode != hvac_mode: if self.hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode super().set_hvac_mode(hvac_mode)
return True return True
else: else:
return False return False
@@ -713,6 +727,13 @@ class UnderlyingClimate(UnderlyingEntity):
return [] return []
return self._underlying_climate.hvac_modes return self._underlying_climate.hvac_modes
@property
def current_humidity(self) -> float | None:
"""Get the humidity"""
if not self.is_initialized:
return None
return self._underlying_climate.current_humidity
@property @property
def fan_modes(self) -> list[str]: def fan_modes(self) -> list[str]:
"""Get the fan_modes""" """Get the fan_modes"""
@@ -847,11 +868,12 @@ class UnderlyingValve(UnderlyingEntity):
_hvac_mode: HVACMode _hvac_mode: HVACMode
# This is the percentage of opening int integer (from 0 to 100) # This is the percentage of opening int integer (from 0 to 100)
_percent_open: int _percent_open: int
_last_sent_temperature = None
def __init__( def __init__(
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying valve"""
super().__init__( super().__init__(
hass=hass, hass=hass,
@@ -865,13 +887,12 @@ class UnderlyingValve(UnderlyingEntity):
self._percent_open = self._thermostat.valve_open_percent self._percent_open = self._thermostat.valve_open_percent
self._valve_entity_id = valve_entity_id self._valve_entity_id = valve_entity_id
async def send_percent_open(self): async def _send_value_to_number(self, number_entity_id: str, value: int):
"""Send the percent open to the underlying valve""" """Send a value to a number entity"""
# This may fails if called after shutdown
try: try:
data = {"value": self._percent_open} data = {"value": value}
target = {ATTR_ENTITY_ID: self._entity_id} target = {ATTR_ENTITY_ID: number_entity_id}
domain = self._entity_id.split(".")[0] domain = number_entity_id.split(".")[0]
await self._hass.services.async_call( await self._hass.services.async_call(
domain=domain, domain=domain,
service=SERVICE_SET_VALUE, service=SERVICE_SET_VALUE,
@@ -883,6 +904,11 @@ class UnderlyingValve(UnderlyingEntity):
# This could happens in unit test if input_number domain is not yet loaded # This could happens in unit test if input_number domain is not yet loaded
# raise err # raise err
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
# This may fails if called after shutdown
return await self._send_value_to_number(self._entity_id, self._percent_open)
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id) _LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
@@ -988,3 +1014,88 @@ class UnderlyingValve(UnderlyingEntity):
def remove_entity(self): def remove_entity(self):
"""Remove the entity after stopping its cycle""" """Remove the entity after stopping its cycle"""
self._cancel_cycle() self._cancel_cycle()
class UnderlyingSonoffTRVZB(UnderlyingValve):
"""A specific underlying class for Sonoff TRVZB TRV"""
_offset_calibration_entity_id: str
_opening_degree_entity_id: str
_closing_degree_entity_id: str
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
offset_calibration_entity_id: str,
opening_degree_entity_id: str,
closing_degree_entity_id: str,
) -> None:
"""Initialize the underlying Sonoff TRV"""
super().__init__(hass, thermostat, opening_degree_entity_id)
self._offset_calibration_entity_id = offset_calibration_entity_id
self._opening_degree_entity_id = opening_degree_entity_id
self._closing_degree_entity_id = closing_degree_entity_id
self._is_min_max_initialized = False
self._max_opening_degree = None
self._min_offset_calibration = None
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
if not self._is_min_max_initialized:
_LOGGER.debug(
"%s - initialize min offset_calibration and max open_degree", self
)
self._max_opening_degree = self._hass.states.get(
self._opening_degree_entity_id
).attributes.get("max")
self._min_offset_calibration = self._hass.states.get(
self._offset_calibration_entity_id
).attributes.get("min")
self._is_min_max_initialized = (
self._max_opening_degree is not None
and self._min_offset_calibration is not None
)
if not self._is_min_max_initialized:
_LOGGER.warning(
"%s - impossible to initialize max_opening_degree or min_offset_calibration. Abort sending percent open to the valve. This could be a temporary message at startup."
)
return
# Send opening_degree
await super().send_percent_open()
# Send closing_degree. TODO 100 hard-coded or take the max of the _closing_degree_entity_id ?
await self._send_value_to_number(
self._closing_degree_entity_id,
self._max_opening_degree - self._percent_open,
)
# send offset_calibration to the min value
await self._send_value_to_number(
self._offset_calibration_entity_id, self._min_offset_calibration
)
@property
def offset_calibration_entity_id(self) -> str:
"""The offset_calibration_entity_id"""
return self._offset_calibration_entity_id
@property
def opening_degree_entity_id(self) -> str:
"""The offset_calibration_entity_id"""
return self._opening_degree_entity_id
@property
def closing_degree_entity_id(self) -> str:
"""The offset_calibration_entity_id"""
return self._closing_degree_entity_id
@property
def hvac_modes(self) -> list[HVACMode]:
"""Get the hvac_modes"""
if not self.is_initialized:
return []
return [HVACMode.OFF, HVACMode.HEAT]

View File

@@ -150,10 +150,11 @@ class VersatileThermostatAPI(dict):
return entity.state return entity.state
return None return None
async def init_vtherm_links(self): async def init_vtherm_links(self, entry_id=None):
"""Initialize all VTherms entities links """Initialize all VTherms entities links
This method is called when HA is fully started (and all entities should be initialized) 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, ...) 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_binary_listener()
await self.reload_central_boiler_entities_list() await self.reload_central_boiler_entities_list()
@@ -175,7 +176,8 @@ class VersatileThermostatAPI(dict):
entity.device_info entity.device_info
and entity.device_info.get("model", None) == DOMAIN 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): async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature""" """Init all VTherm presets when the VTherm uses central temperature"""

View File

@@ -1401,7 +1401,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert vtherm._saved_hvac_mode == HVACMode.HEAT assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 1
assert vtherm.window_state == STATE_ON assert vtherm.window_state == STATE_ON

View File

@@ -630,6 +630,7 @@ async def test_climate_ac_only_change_central_mode_true(
}, },
) )
# 1. set hvac_mode to COOL and preet ECO
with patch("homeassistant.core.ServiceRegistry.async_call"), patch( with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,16 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
) )
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from custom_components.versatile_thermostat.thermostat_climate import ( from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate, ThermostatOverClimate,
) )
from custom_components.versatile_thermostat.switch import (
FollowUnderlyingTemperatureChange,
)
from .commons import * from .commons import *
logging.getLogger().setLevel(logging.DEBUG) 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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101( async def test_underlying_change_follow(
hass: HomeAssistant, hass: HomeAssistant,
skip_hass_states_is_state, skip_hass_states_is_state,
skip_turn_on_off_heater, skip_turn_on_off_heater,
@@ -229,12 +235,27 @@ async def test_bug_101(
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity assert entity
assert entity.name == "TheOverClimateMockName" assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
# because in MockClimate HVACAction is HEATING if hvac_mode is not set # because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert entity.hvac_action is HVACAction.HEATING 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 # Underlying should have been shutdown
assert mock_underlying_set_hvac_mode.call_count == 1 assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls( mock_underlying_set_hvac_mode.assert_has_calls(
@@ -320,6 +341,165 @@ async def test_bug_101(
assert entity.preset_mode is PRESET_NONE 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]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_508( async def test_bug_508(
hass: HomeAssistant, hass: HomeAssistant,
@@ -593,13 +773,26 @@ async def test_ignore_temp_outside_minmax_range(
assert mock_find_climate.mock_calls[0] == call() assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()]) 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) await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT) await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.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 # Wait 11 sec
event_timestamp = now + timedelta(seconds=11) event_timestamp = now + timedelta(seconds=11)
assert entity.is_regulated is False assert entity.is_regulated is False
@@ -607,8 +800,8 @@ async def test_ignore_temp_outside_minmax_range(
entity, entity,
HVACMode.HEAT, HVACMode.HEAT,
HVACMode.HEAT, HVACMode.HEAT,
HVACAction.OFF, HVACAction.HEATING,
HVACAction.OFF, HVACAction.HEATING,
event_timestamp, event_timestamp,
entity.min_temp - 1, entity.min_temp - 1,
True, True,
@@ -616,18 +809,393 @@ async def test_ignore_temp_outside_minmax_range(
) )
assert entity.target_temperature == 17 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) event_timestamp = event_timestamp + timedelta(seconds=11)
assert entity.is_regulated is False assert entity.is_regulated is False
await send_climate_change_event_with_temperature( await send_climate_change_event_with_temperature(
entity, entity,
HVACMode.HEAT, HVACMode.HEAT,
HVACMode.HEAT, HVACMode.HEAT,
HVACAction.OFF, HVACAction.HEATING,
HVACAction.OFF, HVACAction.HEATING,
event_timestamp, event_timestamp,
entity.max_temp + 1, entity.max_temp + 1,
True, True,
"climate.mock_climate", # the underlying climate entity id "climate.mock_climate", # the underlying climate entity id
) )
assert entity.target_temperature == 17 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 == 1
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