Compare commits
84 Commits
6.0.2
...
6.8.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a658b7a2a | ||
|
|
289ccc7bb7 | ||
|
|
c1d1e8f1db | ||
|
|
71c35ecdc0 | ||
|
|
4f8e45dda6 | ||
|
|
d624c327b6 | ||
|
|
b46a24f834 | ||
|
|
d31376d55d | ||
|
|
dbfd294ff3 | ||
|
|
e111bd0647 | ||
|
|
ba69319198 | ||
|
|
f9df925181 | ||
|
|
2d72efe447 | ||
|
|
95af6eba97 | ||
|
|
06dc537767 | ||
|
|
2d79d961dc | ||
|
|
027bf8386b | ||
|
|
a0e548ef71 | ||
|
|
132519b471 | ||
|
|
e6c330fc9d | ||
|
|
968e8286ea | ||
|
|
0f60c070ab | ||
|
|
810430f7b1 | ||
|
|
b4860c2b8d | ||
|
|
60bd522a97 | ||
|
|
fc39cf5f40 | ||
|
|
f6fb7487d5 | ||
|
|
0f585be0c9 | ||
|
|
492c95aff5 | ||
|
|
a530051bbd | ||
|
|
4ef82af8ce | ||
|
|
2ea5cf471b | ||
|
|
f6afaf2715 | ||
|
|
f29b2f9b81 | ||
|
|
de9b95903e | ||
|
|
d112273c58 | ||
|
|
73a9ca4e53 | ||
|
|
1334bdbd8f | ||
|
|
646ef47f6f | ||
|
|
c344c43185 | ||
|
|
062f8a617d | ||
|
|
70f91f3cbe | ||
|
|
668053b352 | ||
|
|
6ff9ff1ee5 | ||
|
|
3f95ed74f4 | ||
|
|
6e42904ddf | ||
|
|
4c1fc396fb | ||
|
|
d6ec7a86be | ||
|
|
a3f8715fe5 | ||
|
|
a1a9a8bbab | ||
|
|
d5c5869276 | ||
|
|
c4b03f8c1e | ||
|
|
ac206a949f | ||
|
|
4bccb746b8 | ||
|
|
e999705286 | ||
|
|
b4873bfd27 | ||
|
|
b00dc09c80 | ||
|
|
da6d6cbce6 | ||
|
|
864e904e21 | ||
|
|
0ee4fe355d | ||
|
|
53dce224cd | ||
|
|
2fd60074c7 | ||
|
|
549423b313 | ||
|
|
6bd1b1137e | ||
|
|
189418e69a | ||
|
|
4ab932f44e | ||
|
|
e1ff23fb30 | ||
|
|
7b657ffabf | ||
|
|
acd22d1fc4 | ||
|
|
d6f33d5796 | ||
|
|
c1ebb46ac6 | ||
|
|
eee4a9c4e3 | ||
|
|
2a3d3ff877 | ||
|
|
a9c368d64c | ||
|
|
1595ff32a2 | ||
|
|
c49545d9e3 | ||
|
|
f4598a407e | ||
|
|
d96fe4bec7 | ||
|
|
a9b87b3aee | ||
|
|
c512cb6f74 | ||
|
|
9269240fe3 | ||
|
|
91ba2387b2 | ||
|
|
162efb4709 | ||
|
|
6a97622226 |
@@ -1,11 +1,30 @@
|
||||
default_config:
|
||||
|
||||
recorder:
|
||||
auto_purge: true
|
||||
purge_keep_days: 1
|
||||
commit_interval: 5
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- number
|
||||
- input_select
|
||||
- versatile_thermostat
|
||||
|
||||
logger:
|
||||
default: warning
|
||||
logs:
|
||||
# custom_components.versatile_thermostat: debug
|
||||
# custom_components.versatile_thermostat.underlyings: debug
|
||||
# custom_components.versatile_thermostat.climate: debug
|
||||
custom_components.versatile_thermostat: debug
|
||||
# custom_components.versatile_thermostat.underlyings: info
|
||||
# custom_components.versatile_thermostat.climate: info
|
||||
# custom_components.versatile_thermostat.base_thermostat: debug
|
||||
custom_components.versatile_thermostat.sensor: info
|
||||
custom_components.versatile_thermostat.binary_sensor: info
|
||||
|
||||
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
|
||||
debugpy:
|
||||
@@ -133,6 +152,7 @@ climate:
|
||||
name: Underlying thermostat2
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat3
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
@@ -166,16 +186,12 @@ climate:
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
|
||||
recorder:
|
||||
commit_interval: 1
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
input_datetime:
|
||||
fake_last_seen:
|
||||
name: Last seen temp sensor
|
||||
icon: mdi:update
|
||||
has_date: true
|
||||
has_time: true
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/issue.md
vendored
2
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -4,6 +4,8 @@ about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
|
||||
|
||||
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
|
||||
|
||||
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -4,7 +4,6 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modifications"
|
||||
},
|
||||
"pylint.lintOnChange": false,
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
|
||||
132
README-fr.md
132
README-fr.md
@@ -13,6 +13,7 @@
|
||||
- [Dans le cas d'une configuration centrale](#dans-le-cas-dune-configuration-centrale)
|
||||
- [Refonte du menu de configuration](#refonte-du-menu-de-configuration)
|
||||
- [Les options de menu 'Configuration incomplète' et 'Finaliser'](#les-options-de-menu-configuration-incomplète-et-finaliser)
|
||||
- [Changements dans la version 5.0](#changements-dans-la-version-50)
|
||||
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
|
||||
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
|
||||
- [Incompatibilités](#incompatibilités)
|
||||
@@ -31,6 +32,7 @@
|
||||
- [Compensation de la température interne](#compensation-de-la-température-interne)
|
||||
- [Synthèse de l'algorithme d'auto-régulation](#synthèse-de-lalgorithme-dauto-régulation)
|
||||
- [Le mode auto-fan](#le-mode-auto-fan)
|
||||
- [Le démarrage / arrêt automatique](#le-démarrage--arrêt-automatique)
|
||||
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
|
||||
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
|
||||
- [Configurer les températures préréglées](#configurer-les-températures-préréglées)
|
||||
@@ -75,6 +77,7 @@
|
||||
- [Dépannages](#dépannages)
|
||||
- [Utilisation d'un Heatzy](#utilisation-dun-heatzy)
|
||||
- [Utilisation d'un radiateur avec un fil pilote](#utilisation-dun-radiateur-avec-un-fil-pilote)
|
||||
- [Utilisation d'un radiateur avec un fil pilote](#utilisation-dun-radiateur-avec-un-fil-pilote-1)
|
||||
- [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe)
|
||||
- [Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne](#le-radiateur-chauffe-alors-que-la-température-de-consigne-est-dépassée-ou-ne-chauffe-pas-alors-que-la-température-de-la-pièce-est-bien-en-dessous-de-la-consigne)
|
||||
- [Type `over_switch` ou `over_valve`](#type-over_switch-ou-over_valve)
|
||||
@@ -91,6 +94,9 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
|
||||
|
||||
>  _*Historique des dernières versions*_
|
||||
> * **Release 6.5** :
|
||||
> - Ajout d'une nouvelle fonction permettant l'arrêt et la relance automatique d'un VTherm `over_climate` [585](https://github.com/jmcollin78/versatile_thermostat/issues/585)
|
||||
> - Amélioration de la gestion des ouvertures au démarrage. Permet de mémoriser et de recalculer l'état d'une ouverture au redémarage de Home Assistant [504](https://github.com/jmcollin78/versatile_thermostat/issues/504)
|
||||
> * **Release 6.0** :
|
||||
> - Ajout d'entités du domaine Number permettant de configurer les températures des presets [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
|
||||
> - Refonte complète du menu de configuration pour supprimer les températures et utililsation d'un menu au lieu d'un tunnel de configuration [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
|
||||
@@ -99,14 +105,14 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
> - ajout de seuils de régulation pour les `over_valve` pour éviter de trop vider la batterie des TRV [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
|
||||
> - ajout d'une option permettant d'utiliser la température interne d'un TRV pour forcer l' auto-régulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
|
||||
> - ajout d'une fonction de keep-alive pour les VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
|
||||
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
|
||||
<details>
|
||||
<summary>Autres versions</summary>
|
||||
|
||||
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
|
||||
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
@@ -125,6 +131,8 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Changements dans la version 6.0</summary>
|
||||
# Changements dans la version 6.0
|
||||
|
||||
## Entités de température pour les pre-réglages
|
||||
@@ -192,10 +200,13 @@ Une fois que toute la configuration est valide, la dernière option se transform
|
||||
Cliquez sur cette option pour créér (resp. modifier) le VTherm :
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Changements dans la version 5.0</summary>
|
||||
|
||||
# Changements dans la version 5.0
|
||||
|
||||
Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez :
|
||||
1. Créer un VTherm de type "Configuration Centrale",
|
||||
2. Saisir les attributs de cette configuration centrale
|
||||
@@ -211,7 +222,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
|
||||
</details>
|
||||
|
||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R. pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l, @jms92100 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
@@ -235,6 +246,8 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
|
||||
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
|
||||
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
|
||||
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
|
||||
6. La clim Airwell avec l'intégration "Midea AC LAN". Si 2 commandes de VTherm sont trop rapprochées, la clim s'arrête d'elle même.
|
||||
7. Les climates basés sur l'intégration Overkiz ne fonctionnent pas. Il parait impossible d'éteindre ni même de changer la température sur ces systèmes.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
@@ -285,9 +298,6 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
||||
> 3. En plus de cette configuration centralisée, tous les VTherm peuvent être contrôlées par une seule entité de type `select`. Cette fonction est nommé `central_mode`. Cela permet de stopper / démarrer / mettre en hors gel / etc tous les VTherms en une seule fois. Pour chaque VTherm, l'utilisateur indique si il est concerné par ce `central_mode`.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Création d'un nouveau Versatile Thermostat</summary>
|
||||
|
||||
## Création d'un nouveau Versatile Thermostat
|
||||
|
||||
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
|
||||
@@ -301,10 +311,6 @@ puis
|
||||
La configuration peut être modifiée via la même interface. Sélectionnez simplement le thermostat à modifier, appuyez sur "Configurer" et vous pourrez modifier certains paramètres ou la configuration.
|
||||
|
||||
Suivez ensuite les étapes de configuration en sélectionnant dans le menu l'option à configurer.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Choix des attributs de base</summary>
|
||||
|
||||
## Choix des attributs de base
|
||||
|
||||
@@ -326,10 +332,6 @@ Donnez les principaux attributs obligatoires :
|
||||
>  _*Notes*_
|
||||
> 1. avec les types ```over_switch``` et ```over_valve```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
|
||||
> 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Sélectionnez des entités pilotées (sous-jacents)</summary>
|
||||
|
||||
## Sélectionnez des entités pilotées (sous-jacents)
|
||||
|
||||
@@ -508,16 +510,23 @@ Il faut évidemment que votre équipement sous-jacent soit équipée d'une venti
|
||||
Si votre équipement ne comprend pas le mode Turbo, le mode Forte` sera utilisé en remplacement.
|
||||
Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie.
|
||||
|
||||
#### Le démarrage / arrêt automatique
|
||||
Cette fonction a été introduite en 6.5.0. Elle permet d'autoriser VTherm a stopper un équipement qui n'a pas besoin d'être allumé et de le redémarrer lorsque les conditions le réclame. Cette fonction est munie de 3 réglages qui permettent d'arrêter / relancer plus ou moins rapidement l'équipement.
|
||||
|
||||
Pour l'utiliser, vous devez :
|
||||
1. Ajouter la fonction `Avec démmarrage et extinction automatique` dans le menu 'Fonctions',
|
||||
2. Paramétrer le niveau de détection dans l'option 'Allumage/extinction automatique' qui s'affiche lorsque la fonction a été activée. Vous choisissez le niveau de détection entre 'Lent', 'Moyen' et 'Rapide'. Les arrêts/relances seront plus nombreux avec le niveau 'Rapide'.
|
||||
|
||||
Une fois paramétré, vous aurez maintenant une nouvelle entité de type `switch` qui vous permet d'autoriser ou non l'arrêt/relance automatique sans toucher à la configuration. Cette entité est disponible sur l'appareil VTherm et se nomme `switch.<name>_enable_auto_start_stop`. Cochez la pour autoriser le démarrage et extinction automatique.
|
||||
|
||||
L'algorithme de détection est décrit [ici](https://github.com/jmcollin78/versatile_thermostat/issues/585).
|
||||
|
||||
### Pour un thermostat de type ```thermostat_over_valve```:
|
||||

|
||||
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
|
||||
|
||||
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurez les coefficients de l'algorithme TPI</summary>
|
||||
|
||||
## Configurez les coefficients de l'algorithme TPI
|
||||
|
||||
@@ -531,10 +540,6 @@ Vous devez donner :
|
||||
|
||||
|
||||
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer les températures préréglées</summary>
|
||||
|
||||
## Configurer les températures préréglées
|
||||
|
||||
@@ -555,10 +560,6 @@ Les pré-réglages se font (depuis v6.0) directement depuis les entités du VThe
|
||||
> 3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
|
||||
> 4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
|
||||
> 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer les portes/fenêtres en allumant/éteignant les thermostats</summary>
|
||||
|
||||
## Configurer les portes/fenêtres en allumant/éteignant les thermostats
|
||||
|
||||
@@ -601,10 +602,6 @@ Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvert
|
||||
> 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
|
||||
> 3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
|
||||
> 4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer le mode d'activité ou la détection de mouvement</summary>
|
||||
|
||||
## Configurer le mode d'activité ou la détection de mouvement
|
||||
|
||||
@@ -632,10 +629,6 @@ Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activ
|
||||
|
||||
>  _*Notes*_
|
||||
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer la gestion de la puissance</summary>
|
||||
|
||||
## Configurer la gestion de la puissance
|
||||
|
||||
@@ -654,10 +647,6 @@ Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'u
|
||||
> 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
|
||||
> 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
|
||||
> 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configurer la présence (ou l'absence)</summary>
|
||||
|
||||
## Configurer la présence (ou l'absence)
|
||||
|
||||
@@ -679,10 +668,6 @@ ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de
|
||||
>  _*Notes*_
|
||||
> 1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
|
||||
> 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configuration avancée</summary>
|
||||
|
||||
## Configuration avancée
|
||||
|
||||
@@ -700,6 +685,8 @@ Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque
|
||||
|
||||
Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
|
||||
|
||||
Note: les paramètres `security_min_on_percent` et `security_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
|
||||
|
||||
Depuis la version 5.3 il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
|
||||
```
|
||||
versatile_thermostat:
|
||||
@@ -717,10 +704,6 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
|
||||
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
|
||||
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
|
||||
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Le contrôle centralisé</summary>
|
||||
|
||||
## Le contrôle centralisé
|
||||
|
||||
@@ -737,10 +720,6 @@ Il est donc possible de contrôler tous les VTherms (que ceux que l'on désigne
|
||||
Exemple de rendu :
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Le contrôle d'une chaudière centrale</summary>
|
||||
|
||||
## Le contrôle d'une chaudière centrale
|
||||
|
||||
@@ -842,7 +821,6 @@ context:
|
||||
|
||||
>  _*Notes*_
|
||||
> Le contrôle par du logiciel ou du matériel de type domotique d'une chaudière centrale peut induire des risques pour son bon fonctionnement. Assurez-vous avant d'utiliser ces fonctions, que votre chaudière possède bien des fonctions de sécurité et que celles-ci fonctionnent. Allumer une chaudière si tous les robinets sont fermés peut générer de la sur-pression par exemple.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Synthèse des paramètres</summary>
|
||||
@@ -909,6 +887,8 @@ context:
|
||||
| ``central_boiler_activation_service`` | Service d'activation de la chaudière | - | - | - | X |
|
||||
| ``central_boiler_deactivation_service`` | Service de desactivation de la chaudière | - | - | - | X |
|
||||
| ``used_by_controls_central_boiler`` | Indique si le VTherm contrôle la chaudière centrale | X | X | X | - |
|
||||
| ``use_auto_start_stop_feature`` | Indique si la fonction de démarrage/extinction automatique est activée | - | X | - | - |
|
||||
| ``auto_start_stop_lvel`` | Le niveau de détection de l'auto start/stop | - | X | - | - |
|
||||
</details>
|
||||
|
||||
# Exemples de réglage
|
||||
@@ -1166,6 +1146,9 @@ Les attributs personnalisés sont les suivants :
|
||||
| ``is_controlled_by_central_mode`` | True si le VTherm peut être controlé de façon centrale |
|
||||
| ``last_central_mode`` | Le dernier mode central utilisé (None si le VTherm n'est pas controlé en central) |
|
||||
| ``is_used_by_central_boiler`` | Indique si le VTherm peut contrôler la chaudière centrale |
|
||||
| ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter |
|
||||
| ``auto_start_stop_level`` | Indique le niveau d'auto start/stop |
|
||||
| ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel |
|
||||
|
||||
# Quelques résultats
|
||||
|
||||
@@ -1200,9 +1183,9 @@ Une carte spéciale pour le Versatile Thermostat a été développée (sur la ba
|
||||
## Encore mieux avec le composant Scheduler !
|
||||
|
||||
Afin de profiter de toute la puissance du Versatile Thermostat, je vous invite à l'utiliser avec https://github.com/nielsfaber/scheduler-component
|
||||
En effet, le composant scheduler propose une gestion de la base climatique sur les modes prédéfinis. Cette fonctionnalité a un intérêt limité avec le thermostat générique mais elle devient très puissante avec le thermostat Awesome :
|
||||
En effet, le composant scheduler propose une gestion de la base climatique sur les modes prédéfinis. Cette fonctionnalité a un intérêt limité avec le thermostat générique mais elle devient très puissante avec le Versatile Thermostat :
|
||||
|
||||
À partir d'ici, je suppose que vous avez installé Awesome Thermostat et Scheduler Component.
|
||||
À partir d'ici, je suppose que vous avez installé Versatile Thermostat et Scheduler Component.
|
||||
|
||||
Dans Scheduler, ajoutez un planning :
|
||||
|
||||
@@ -1497,6 +1480,43 @@ Exemple :
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Utilisation d'un radiateur avec un module Nodon</summary>
|
||||
|
||||
## Utilisation d'un radiateur avec un fil pilote
|
||||
Comme pour le Heatzy ci-dessus vous pouvez utiliser un switch virtuel qui va changer le preset de votre radiateur en fonction de l'état d'allumage du VTherm.
|
||||
Exemple :
|
||||
```
|
||||
- platform: template
|
||||
switches:
|
||||
chauffage_chb_parents:
|
||||
unique_id: chauffage_chb_parents
|
||||
friendly_name: Chauffage chambre parents
|
||||
value_template: "{{ is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') }}"
|
||||
icon_template: >-
|
||||
{% if is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') %}
|
||||
mdi:radiator
|
||||
{% elif is_state('select.fp_chb_parents_pilot_wire_mode', 'frost_protection') %}
|
||||
mdi:snowflake
|
||||
{% else %}
|
||||
mdi:radiator-disabled
|
||||
{% endif %}
|
||||
turn_on:
|
||||
service: select.select_option
|
||||
target:
|
||||
entity_id: select.fp_chb_parents_pilot_wire_mode
|
||||
data:
|
||||
option: comfort
|
||||
turn_off:
|
||||
service: select.select_option
|
||||
target:
|
||||
entity_id: select.fp_chb_parents_pilot_wire_mode
|
||||
data:
|
||||
option: eco
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Seul le premier radiateur chauffe</summary>
|
||||
|
||||
@@ -1551,7 +1571,7 @@ Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les u
|
||||
<summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
|
||||
|
||||
## Pourquoi mon Versatile Thermostat se met en Securite ?
|
||||
Le mode sécurité n'est possible que sur les VTherm `over_switch` et `over_valve`. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
|
||||
Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
|
||||
|
||||
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
|
||||
|
||||
|
||||
366
README.md
366
README.md
@@ -13,7 +13,7 @@
|
||||
- [In the case of a central configuration](#in-the-case-of-a-central-configuration)
|
||||
- [Redesign of the configuration menu](#redesign-of-the-configuration-menu)
|
||||
- [The 'Incomplete configuration' and 'Finalize' menu options](#the-incomplete-configuration-and-finalize-menu-options)
|
||||
- [Changements dans la version 5.0](#changements-dans-la-version-50)
|
||||
- [Changes in release 5.0](#changes-in-release-50)
|
||||
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
|
||||
- [When to use / not use](#when-to-use--not-use)
|
||||
- [Incompatibilities](#incompatibilities)
|
||||
@@ -32,6 +32,7 @@
|
||||
- [Internal temperature compensation](#internal-temperature-compensation)
|
||||
- [synthesis of the self-regulation algorithm](#synthesis-of-the-self-regulation-algorithm)
|
||||
- [Auto-fan mode](#auto-fan-mode)
|
||||
- [Automatic start/stop](#automatic-startstop)
|
||||
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
|
||||
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
|
||||
- [Configure the preset temperature](#configure-the-preset-temperature)
|
||||
@@ -76,6 +77,7 @@
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Using a Heatzy](#using-a-heatzy)
|
||||
- [Using a Heatsink with a Pilot Wire](#using-a-heatsink-with-a-pilot-wire)
|
||||
- [Using a heater with a Nodon module](#using-a-heater-with-a-nodon-module)
|
||||
- [Only the first radiator heats](#only-the-first-radiator-heats)
|
||||
- [The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint](#the-radiator-heats-up-even-though-the-setpoint-temperature-is-exceeded-or-does-not-heat-up-even-though-the-room-temperature-is-well-below-the-setpoint)
|
||||
- [Type `over_switch` or `over_valve`](#type-over_switch-or-over_valve)
|
||||
@@ -92,6 +94,9 @@
|
||||
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
|
||||
|
||||
> _*Latest releases*_
|
||||
> * **Release 6.5** :
|
||||
> - Added a new function allowing the automatic shutdown and restart of a VTherm `over_climate` [585](https://github.com/jmcollin78/versatile_thermostat/issues/585)
|
||||
> - Improved management of openings at startup. Allows to memorize and recalculate the state of an opening when restarting Home Assistant [504](https://github.com/jmcollin78/versatile_thermostat/issues/504)
|
||||
> * **Release 6.0**:
|
||||
> - Added entities from the Number domain to configure preset temperatures [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
|
||||
> - Complete redesign of the configuration menu to remove temperatures and use a menu instead of a configuration tunnel [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
|
||||
@@ -100,13 +105,13 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
|
||||
> - addition of regulation thresholds for the `over_valve` to avoid draining the TRV battery too much [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
|
||||
> - added an option allowing the internal temperature of a TRV to be used to force self-regulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
|
||||
> - added a keep-alive function for VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
|
||||
<details>
|
||||
<summary>Others releases</summary>
|
||||
|
||||
> * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
|
||||
> * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
<details>
|
||||
<summary>Others releases</summary>
|
||||
|
||||
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
|
||||
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
@@ -125,6 +130,10 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
|
||||
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Changes in version 6.0</summary>
|
||||
|
||||
# Changes in version 6.0
|
||||
|
||||
## Temperature entities for presets
|
||||
@@ -192,11 +201,12 @@ Once all configuration is valid, the last option changes to:
|
||||
Click on this option to create (resp. modify) the VTherm:
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Changements dans la version 5.0</summary>
|
||||
<summary>Changes in release 5.0</summary>
|
||||
|
||||
# Changements dans la version 5.0
|
||||
# Changes in release 5.0
|
||||
|
||||
You can now define a central configuration which will allow you to share certain attributes on all your VTherms (or only part of them). To use this possibility, you must:
|
||||
1. Create a VTherm of type “Central Configuration”,
|
||||
@@ -212,7 +222,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
|
||||
</details>
|
||||
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R. for the beers. It's very nice and encourages me to continue!
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l, @jms92100 for the beers. It's very nice and encourages me to continue!
|
||||
|
||||
# When to use / not use
|
||||
This thermostat can control 3 types of equipment:
|
||||
@@ -236,6 +246,8 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
|
||||
3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
|
||||
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
|
||||
5. TRV of type Aqara SRTS-A01 and MOES TV01-ZB which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
|
||||
6. The Airwell with the "Midea AC LAN" integration. If two orders are too close, the device shut off.
|
||||
7. System based on intégration Overkiz don't work as expected. It seems not possible to turn off nor sending setpoint on those systems.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
@@ -286,9 +298,6 @@ This component named __Versatile thermostat__ manage the following use cases :
|
||||
> 3. In addition to this centralized configuration, all VTherms can be controlled by a single entity of type `select`. This function is named `central_mode`. This allows you to stop / start / freeze / etc. all VTherms at once. For each VTherm, the user indicates whether he is affected by this `central_mode`.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Creation of a new Versatile Thermostat</summary>
|
||||
|
||||
## Creation of a new Versatile Thermostat
|
||||
|
||||
Click on Add integration button in the integration page
|
||||
@@ -299,11 +308,6 @@ The configuration can be change through the same interface. Simply select the th
|
||||
Then choose the type of VTherm you want to create:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Minimal configuration update</summary>
|
||||
|
||||
## Minimal configuration update
|
||||
|
||||
Then choose the “Main attributes” menu.
|
||||
@@ -324,10 +328,6 @@ Give the main mandatory attributes:
|
||||
>  _*Notes*_
|
||||
> 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
|
||||
> 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Select the driven entity</summary>
|
||||
|
||||
## Select the driven entity
|
||||
|
||||
@@ -389,98 +389,17 @@ These three parameters make it possible to modulate the regulation and avoid mul
|
||||
|
||||
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
|
||||
|
||||
#### Self-regulation in Expert mode
|
||||
|
||||
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
|
||||
The adjustable parameters are as follows:
|
||||
1. `kp`: the factor applied to the raw error,
|
||||
2. `ki`: the factor applied to the accumulation of errors,
|
||||
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
|
||||
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
|
||||
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
|
||||
6. `accumulated_error_threshold`: the maximum for error accumulation.
|
||||
|
||||
For tuning, these observations must be taken into account:
|
||||
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
|
||||
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
|
||||
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
|
||||
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
|
||||
|
||||
The pre-programmed values are as follows:
|
||||
|
||||
Slow régulation :
|
||||
|
||||
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
|
||||
Light régulation :
|
||||
|
||||
kp: 0.2
|
||||
ki: 0.05
|
||||
k_ext: 0.05
|
||||
offset_max: 1.5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 10
|
||||
|
||||
Medium régulation :
|
||||
|
||||
kp: 0.3
|
||||
ki: 0.05
|
||||
k_ext: 0.1
|
||||
offset_max: 2
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 20
|
||||
|
||||
Strong régulation :
|
||||
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
|
||||
```
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
```
|
||||
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
|
||||
|
||||
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
|
||||
|
||||
#### Internal temperature compensation
|
||||
Sometimes, it happens that the internal thermometer of the underlying (TRV, air conditioning, etc.) is so wrong that self-regulation is not enough to regulate.
|
||||
This happens when the internal thermometer is too close to the heat source. The internal temperature then rises much faster than the room temperature, which generates faults in the regulation.
|
||||
Example :
|
||||
1. the room temperature is 18°, the setpoint is 20°,
|
||||
2. the internal temperature of the equipment is 22°,
|
||||
3. if VTherm sends 21° as setpoint (= 20° + 1° auto-regulation), then the equipment will not heat because its internal temperature (22°) is above the setpoint (21°)
|
||||
Sometimes, a device’s internal temperature sensor (like in a TRV or AC) can give inaccurate readings, especially if it’s too close to a heat source. This can cause the device to stop heating too soon.
|
||||
For example:
|
||||
1. target temperature: 20 °C, room temperature: 18 °C,
|
||||
2. device’s internal sensor: 22 °C
|
||||
3. If the target temperature is increased to 21 °C, the device won’t heat because it thinks it’s already warm (internal temperature is 22°C).
|
||||
|
||||
To overcome this, a new optional option was added in version 5.4: 
|
||||
The Adjust Setpoint for Room vs. TRV Temperature feature fixes this by adding the temperature difference between the room and the device’s internal reading to the target. In this case, VTherm would adjust the target to 25°C (21°C + 4°C difference), forcing the device to continue heating.
|
||||
|
||||
When enabled, this function will add the difference between the internal temperature and the room temperature to the setpoint to force heating.
|
||||
In the example above, the difference is +4° (22° - 18°), so VTherm will send 25° (21°+4°) to the equipment forcing it to heat up.
|
||||
|
||||
This difference is calculated for each underlying because each has its own internal temperature. Think of a VTherm which would be connected to 3 TRVs each with its internal temperature for example.
|
||||
|
||||
We then obtain much more effective self-regulation which avoids the pitfall of large variations in faulty internal temperature.
|
||||
This adjustment is specific to each device, making the heating system more accurate and avoiding issues from faulty sensor readings.
|
||||
See 
|
||||
|
||||
#### synthesis of the self-regulation algorithm
|
||||
The self-regulation algorithm can be summarized as follows:
|
||||
@@ -503,6 +422,17 @@ Obviously your underlying equipment must be equipped with ventilation and be con
|
||||
If your equipment does not include Turbo mode, Forte` mode will be used as a replacement.
|
||||
Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen.
|
||||
|
||||
#### Automatic start/stop
|
||||
This function was introduced in 6.5.0. It allows VTherm to stop equipment that does not need to be turned on and to restart it when conditions require it. This function has 3 settings that allow the equipment to be stopped/restarted more or less quickly.
|
||||
|
||||
To use it, you must:
|
||||
1. Add the `Use the auto start and stop feature` function in the 'Features' menu,
|
||||
2. Set the detection level in the `Auto start and stop` option that is displayed when the function has been activated. You choose the detection level between 'Slow', 'Medium' and 'Fast'. The 'Fast' level will result in more shutdowns/restarts.
|
||||
|
||||
Once configured, you will now have a new entity of type `switch` that allows you to authorize or not the automatic shutdown/restart without touching the configuration. This entity is available on the VTherm device and is called `switch.<name>_enable_auto_start_stop`. Check it to authorize the automatic startup and shutdown.
|
||||
|
||||
The detection algorithm is described [here](https://github.com/jmcollin78/versatile_thermostat/issues/585).
|
||||
|
||||
### For a thermostat of type ```thermostat_over_valve```:
|
||||

|
||||
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
|
||||
@@ -510,11 +440,6 @@ The algorithm to use is currently limited to TPI is available. See [algorithm](#
|
||||
|
||||
It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the TPI algorithm coefficients</summary>
|
||||
|
||||
## Configure the TPI algorithm coefficients
|
||||
|
||||
Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the "TPI" menu option, you will get there:
|
||||
@@ -522,11 +447,6 @@ Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the
|
||||
|
||||
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the preset temperature</summary>
|
||||
|
||||
## Configure the preset temperature
|
||||
|
||||
The preset mode allows you to pre-configurate targeted temperature. Used in conjonction with Scheduler (see [scheduler](#even-better-with-scheduler-component) you will have a powerfull and simple way to optimize the temperature vs electrical consumption of your hous. Preset handled are the following :
|
||||
@@ -547,11 +467,6 @@ The pre-settings are made (since v6.0) directly from the VTherm entities or from
|
||||
> 4. if you uses the advanced configuration you will see the preset set to ``safety`` if the temperature could not be retrieved after a certain delay
|
||||
> 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the doors/windows turning on/off the thermostats</summary>
|
||||
|
||||
## Configure the doors/windows turning on/off the thermostats
|
||||
|
||||
You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
|
||||
@@ -593,11 +508,6 @@ And that's all ! your thermostat will turn off when the windows are open and tur
|
||||
> 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
|
||||
> 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configure the activity mode or motion detection</summary>
|
||||
|
||||
## Configure the activity mode or motion detection
|
||||
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
|
||||

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

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Control of a central boiler</summary>
|
||||
|
||||
## Control of a central boiler
|
||||
|
||||
Since release 5.3, you have the possibility of controlling a centralized boiler. From the moment it is possible to start or stop this boiler from Home Assistant, then Versatile Thermostat will be able to control it directly.
|
||||
@@ -836,7 +724,112 @@ context:
|
||||
>  _*Notes*_
|
||||
> Controlling a central boiler using software or hardware such as home automation can pose risks to its proper functioning. Before using these functions, make sure that your boiler has safety functions and that they are working. Turning on a boiler if all the taps are closed can generate excess pressure, for example.
|
||||
|
||||
</details>
|
||||
|
||||
## Expert Mode Settings
|
||||
|
||||
Expert Mode settings refer to Settings made in the Home Assistant `configuration.yaml` file under the `versatile_thermostat` section. You might have to add this section by yourself to the `configuration.yaml` file.
|
||||
|
||||
These settings are meant to be used only in **specific niche cases and with careful considerations**.
|
||||
|
||||
|
||||
The following sections describe the available export mode settings in detail with examples on how to configure them. Be aware that these settings require a **complete restart** of Home Assistant or a **reload of Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat) to take effect.
|
||||
|
||||
|
||||
### Self-regulation in Expert mode
|
||||
|
||||
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
|
||||
The adjustable parameters are as follows:
|
||||
1. `kp`: the factor applied to the raw error,
|
||||
2. `ki`: the factor applied to the accumulation of errors,
|
||||
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
|
||||
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
|
||||
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
|
||||
6. `accumulated_error_threshold`: the maximum for error accumulation.
|
||||
|
||||
For tuning, these observations must be taken into account:
|
||||
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
|
||||
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
|
||||
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
|
||||
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
|
||||
|
||||
The pre-programmed values are as follows:
|
||||
|
||||
Slow régulation :
|
||||
|
||||
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
|
||||
Light régulation :
|
||||
|
||||
kp: 0.2
|
||||
ki: 0.05
|
||||
k_ext: 0.05
|
||||
offset_max: 1.5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 10
|
||||
|
||||
Medium régulation :
|
||||
|
||||
kp: 0.3
|
||||
ki: 0.05
|
||||
k_ext: 0.1
|
||||
offset_max: 2
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 20
|
||||
|
||||
Strong régulation :
|
||||
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
|
||||
```
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
```
|
||||
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
|
||||
|
||||
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
|
||||
|
||||
|
||||
### On Time Clamping (max_on_percent)
|
||||
|
||||
|
||||
The calculated on time percent can be limited to a maximum percentage of the cycle duration. This setting has to be made in expert mode and will be used for all Versatile Thermostats.
|
||||
|
||||
```
|
||||
versatile_thermostat:
|
||||
max_on_percent: 0.8
|
||||
```
|
||||
|
||||
The example above limits the maximum ON time to 80% (0.8) of the cycle length. If the cycle length is for example 600 seconds (10min), the maximum ON time will be limited to 480 seconds (8min). The remaining 120 seconds of the cycle will always remain in the OFF state.
|
||||
|
||||
There are three debug attributes of interest regarding this feature:
|
||||
|
||||
* `max_on_percent` # clamping setting as configured in expert mode
|
||||
* `calculated_on_percent` # calculated on percent without clamping applied
|
||||
* `on_percent` # used on percent with clamping applied
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Parameter summary</summary>
|
||||
@@ -903,6 +896,8 @@ context:
|
||||
| ``central_boiler_activation_service`` | Activation service of the boiler | - | - | - | X |
|
||||
| ``central_boiler_deactivation_service`` | Deactivaiton service of the boiler | - | - | - | X |
|
||||
| ``used_by_controls_central_boiler`` | Indicate if the VTherm control the central boiler | X | X | X | - |
|
||||
| ``use_auto_start_stop_feature`` | Indique si la fonction de démarrage/extinction automatique est activée | - | X | - | - |
|
||||
| ``auto_start_stop_lvel`` | Le niveau de détection de l'auto start/stop | - | X | - | - |
|
||||
</details>
|
||||
|
||||
# Tuning examples
|
||||
@@ -1158,6 +1153,9 @@ Custom attributes are the following:
|
||||
| ``is_controlled_by_central_mode`` | True if the VTherm can be centrally controlled |
|
||||
| ``last_central_mode`` | The last central mode used (None if the VTherm is not centrally controlled) |
|
||||
| ``is_used_by_central_boiler`` | Indicate if the VTherm can control the central boiler |
|
||||
| ``auto_start_stop_enable`` | Indicate if the VTherm is allowed to do auto start and stop |
|
||||
| ``auto_start_stop_level`` | Give the level of auto start/stop |
|
||||
| ``hvac_off_reason`` | Give the reason of stop of the VTherm. This could be Window, Auto-start/stop or Manual |
|
||||
|
||||
# Some results
|
||||
|
||||
@@ -1281,9 +1279,13 @@ Replace values in [[ ]] by yours.
|
||||
yaxis: y1
|
||||
name: Ema
|
||||
- entity: '[[climate]]'
|
||||
attribute: regulated_target_temperature
|
||||
yaxis: y1
|
||||
name: Regulated T°
|
||||
attribute: on_percent
|
||||
yaxis: y2
|
||||
name: Power percent
|
||||
fill: tozeroy
|
||||
fillcolor: rgba(200, 10, 10, 0.3)
|
||||
line:
|
||||
color: rgba(200, 10, 10, 0.9)
|
||||
- entity: '[[slope]]'
|
||||
name: Slope
|
||||
fill: tozeroy
|
||||
@@ -1308,12 +1310,19 @@ Replace values in [[ ]] by yours.
|
||||
yaxis:
|
||||
visible: true
|
||||
position: 0
|
||||
yaxis2:
|
||||
visible: true
|
||||
position: 0
|
||||
fixedrange: true
|
||||
range:
|
||||
- 0
|
||||
- 1
|
||||
yaxis9:
|
||||
visible: true
|
||||
fixedrange: false
|
||||
range:
|
||||
- -0.5
|
||||
- 0.5
|
||||
- -2
|
||||
- 2
|
||||
position: 1
|
||||
xaxis:
|
||||
rangeselector:
|
||||
@@ -1336,7 +1345,7 @@ Example of graph obtained with Plotly :
|
||||
|
||||
|
||||
## And always better and better with the NOTIFIER daemon app to notify events
|
||||
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
|
||||
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
|
||||
|
||||
This is a great example of using the notifications described here [notification](#notifications).
|
||||
|
||||
@@ -1488,6 +1497,43 @@ Example :
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using a heater with a Nodon</summary>
|
||||
|
||||
## Using a heater with a Nodon module
|
||||
As for the heatzy module above you can use a virtual switch which will change the preset of your heater depending of the state of the VTherm.
|
||||
Example :
|
||||
```
|
||||
- platform: template
|
||||
switches:
|
||||
chauffage_chb_parents:
|
||||
unique_id: chauffage_chb_parents
|
||||
friendly_name: Chauffage chambre parents
|
||||
value_template: "{{ is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') }}"
|
||||
icon_template: >-
|
||||
{% if is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') %}
|
||||
mdi:radiator
|
||||
{% elif is_state('select.fp_chb_parents_pilot_wire_mode', 'frost_protection') %}
|
||||
mdi:snowflake
|
||||
{% else %}
|
||||
mdi:radiator-disabled
|
||||
{% endif %}
|
||||
turn_on:
|
||||
service: select.select_option
|
||||
target:
|
||||
entity_id: select.fp_chb_parents_pilot_wire_mode
|
||||
data:
|
||||
option: comfort
|
||||
turn_off:
|
||||
service: select.select_option
|
||||
target:
|
||||
entity_id: select.fp_chb_parents_pilot_wire_mode
|
||||
data:
|
||||
option: eco
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Only the first radiator heats</summary>
|
||||
|
||||
@@ -1542,7 +1588,7 @@ These parameters are sensitive and quite difficult to adjust. Please only use th
|
||||
|
||||
## Why does my Versatile Thermostat go into Safety?
|
||||
|
||||
Safety mode is only possible on VTherm `over_switch` and `over_valve`. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`.
|
||||
Safety mode is possible on all VTherm's type. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`.
|
||||
|
||||
As the algorithm is based on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To avoid this, when the conditions mentioned above are detected, heating is limited to the `security_default_on_percent` parameter. This value must therefore be reasonably low. It helps prevent a fire while avoiding completely cutting off the radiator (risk of freezing).
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigType
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
@@ -37,6 +38,23 @@ from .const import (
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
@@ -69,6 +87,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
|
||||
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
|
||||
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
|
||||
vol.Optional(CONF_MAX_ON_PERCENT): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
},
|
||||
@@ -107,13 +126,16 @@ async def async_setup(
|
||||
"VersatileThermostat - HA is started, initialize all links between VTherm entities"
|
||||
)
|
||||
await api.init_vtherm_links()
|
||||
await api.notify_central_mode_change()
|
||||
await api.reload_central_boiler_entities_list()
|
||||
|
||||
if hass.state == CoreState.running:
|
||||
await _async_startup_internal()
|
||||
else:
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_startup_internal)
|
||||
|
||||
hass.helpers.service.async_register_admin_service(
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RELOAD,
|
||||
_handle_reload,
|
||||
@@ -156,14 +178,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
if hass.state == CoreState.running:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links(entry.entry_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Calling update_listener entry: entry_id='%s', value='%s'",
|
||||
entry.entry_id,
|
||||
entry.data,
|
||||
)
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
await reload_all_vtherm(hass)
|
||||
else:
|
||||
@@ -172,7 +202,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api is not None:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
await api.init_vtherm_links(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -203,10 +233,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
)
|
||||
new = {**config_entry.data}
|
||||
|
||||
if (
|
||||
config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
== CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
):
|
||||
thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
if thermostat_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
new[CONF_USE_WINDOW_FEATURE] = True
|
||||
new[CONF_USE_MOTION_FEATURE] = True
|
||||
new[CONF_USE_POWER_FEATURE] = new.get(CONF_POWER_SENSOR, None) is not None
|
||||
@@ -218,6 +247,50 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"add_central_boiler_control", False
|
||||
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
|
||||
|
||||
if config_entry.data.get(CONF_UNDERLYING_LIST, None) is None:
|
||||
underlying_list = []
|
||||
if thermostat_type == CONF_THERMOSTAT_SWITCH:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_HEATER, None),
|
||||
config_entry.data.get(CONF_HEATER_2, None),
|
||||
config_entry.data.get(CONF_HEATER_3, None),
|
||||
config_entry.data.get(CONF_HEATER_4, None),
|
||||
]
|
||||
elif thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_CLIMATE, None),
|
||||
config_entry.data.get(CONF_CLIMATE_2, None),
|
||||
config_entry.data.get(CONF_CLIMATE_3, None),
|
||||
config_entry.data.get(CONF_CLIMATE_4, None),
|
||||
]
|
||||
elif thermostat_type == CONF_THERMOSTAT_VALVE:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_VALVE, None),
|
||||
config_entry.data.get(CONF_VALVE_2, None),
|
||||
config_entry.data.get(CONF_VALVE_3, None),
|
||||
config_entry.data.get(CONF_VALVE_4, None),
|
||||
]
|
||||
|
||||
new[CONF_UNDERLYING_LIST] = [
|
||||
entity for entity in underlying_list if entity is not None
|
||||
]
|
||||
|
||||
for key in [
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
]:
|
||||
new.pop(key, None)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" This file implements the Auto start/stop algorithm as described here: https://github.com/jmcollin78/versatile_thermostat/issues/585
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
TYPE_AUTO_START_STOP_LEVELS,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Some constant to make algorithm depending of level
|
||||
DT_MIN = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 30,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM: 15,
|
||||
AUTO_START_STOP_LEVEL_FAST: 7,
|
||||
}
|
||||
|
||||
# the measurement cycle (2 min)
|
||||
CYCLE_SEC = 120
|
||||
|
||||
# A temp hysteresis to avoid rapid OFF/ON
|
||||
TEMP_HYSTERESIS = 0.5
|
||||
|
||||
ERROR_THRESHOLD = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
|
||||
AUTO_START_STOP_LEVEL_MEDIUM: 5, # 5 cycle above 1° or 3 cycle above 2°, ..., 1 cycle above 5°
|
||||
AUTO_START_STOP_LEVEL_FAST: 2, # 2 cycle above 1° or 1 cycle above 2°
|
||||
}
|
||||
|
||||
AUTO_START_STOP_ACTION_OFF = "turnOff"
|
||||
AUTO_START_STOP_ACTION_ON = "turnOn"
|
||||
AUTO_START_STOP_ACTION_NOTHING = "nothing"
|
||||
AUTO_START_STOP_ACTIONS = Literal[ # pylint: disable=invalid-name
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
AUTO_START_STOP_ACTION_NOTHING,
|
||||
]
|
||||
|
||||
class AutoStartStopDetectionAlgorithm:
|
||||
"""The class that implements the algorithm listed above"""
|
||||
|
||||
_dt: float | None = None
|
||||
_level: str = AUTO_START_STOP_LEVEL_NONE
|
||||
_accumulated_error: float = 0
|
||||
_error_threshold: float | None = None
|
||||
_last_calculation_date: datetime | None = None
|
||||
|
||||
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
|
||||
"""Initalize a new algorithm with the right constants"""
|
||||
self._vtherm_name = vtherm_name
|
||||
self._init_level(level)
|
||||
|
||||
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||
"""Initialize a new level"""
|
||||
if level == self._level:
|
||||
return
|
||||
|
||||
self._level = level
|
||||
if self._level != AUTO_START_STOP_LEVEL_NONE:
|
||||
self._dt = DT_MIN[level]
|
||||
self._error_threshold = ERROR_THRESHOLD[level]
|
||||
# reset accumulated error if we change the level
|
||||
self._accumulated_error = 0
|
||||
|
||||
def calculate_action(
|
||||
self,
|
||||
hvac_mode: HVACMode | None,
|
||||
saved_hvac_mode: HVACMode | None,
|
||||
target_temp: float,
|
||||
current_temp: float,
|
||||
slope_min: float | None,
|
||||
now: datetime,
|
||||
) -> AUTO_START_STOP_ACTIONS:
|
||||
"""Calculate an eventual action to do depending of the value in parameter"""
|
||||
if self._level == AUTO_START_STOP_LEVEL_NONE:
|
||||
_LOGGER.debug(
|
||||
"%s - auto-start/stop is disabled",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s",
|
||||
self,
|
||||
hvac_mode,
|
||||
saved_hvac_mode,
|
||||
target_temp,
|
||||
current_temp,
|
||||
slope_min,
|
||||
now,
|
||||
)
|
||||
|
||||
if hvac_mode is None or target_temp is None or current_temp is None:
|
||||
_LOGGER.debug(
|
||||
"%s - No all mandatory parameters are set. Disable auto-start/stop",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = target_temp - current_temp
|
||||
|
||||
# reduce the error considering the dt between the last measurement
|
||||
if self._last_calculation_date is not None:
|
||||
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
|
||||
# ignore two calls too near (< 24 sec)
|
||||
if dtmin <= 0.2:
|
||||
_LOGGER.debug(
|
||||
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
|
||||
self,
|
||||
now,
|
||||
self._last_calculation_date,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
error = error * dtmin
|
||||
|
||||
# If the error have change its sign, reset smoothly the accumulated error
|
||||
if error * self._accumulated_error < 0:
|
||||
self._accumulated_error = self._accumulated_error / 2.0
|
||||
|
||||
self._accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self._accumulated_error = min(
|
||||
self._error_threshold,
|
||||
max(-self._error_threshold, self._accumulated_error),
|
||||
)
|
||||
|
||||
self._last_calculation_date = now
|
||||
|
||||
temp_at_dt = current_temp + slope_min * self._dt
|
||||
|
||||
# Check to turn-off
|
||||
# When we hit the threshold, that mean we can turn off
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
if (
|
||||
self._accumulated_error <= -self._error_threshold
|
||||
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for heating for a long time.",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug("%s - nothing to do, we are heating", self)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
if (
|
||||
self._accumulated_error >= self._error_threshold
|
||||
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for cooling for a long time.",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we are cooling",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# check to turn on
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
|
||||
if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to heat",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we don't need to heat soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
|
||||
if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to cool",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we don't need to cool soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, no conditions applied",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
def set_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||
"""Set a new level"""
|
||||
self._init_level(level)
|
||||
|
||||
@property
|
||||
def dt_min(self) -> float:
|
||||
"""Get the dt value"""
|
||||
return self._dt
|
||||
|
||||
@property
|
||||
def accumulated_error(self) -> float:
|
||||
"""Get the accumulated error value"""
|
||||
return self._accumulated_error
|
||||
|
||||
@property
|
||||
def accumulated_error_threshold(self) -> float:
|
||||
"""Get the accumulated error threshold value"""
|
||||
return self._error_threshold
|
||||
|
||||
@property
|
||||
def level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Get the level value"""
|
||||
return self._level
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@ from .const import (
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
overrides,
|
||||
@@ -63,10 +64,13 @@ async def async_setup_entry(
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
entities = None
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
entities = [
|
||||
CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
|
||||
entities = [
|
||||
CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
else:
|
||||
entities = [
|
||||
SecurityBinarySensor(hass, unique_id, name, entry.data),
|
||||
@@ -81,7 +85,8 @@ async def async_setup_entry(
|
||||
if entry.data.get(CONF_USE_POWER_FEATURE):
|
||||
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@@ -95,7 +100,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the SecurityState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Security state"
|
||||
self._attr_unique_id = f"{self._device_name}_security_state"
|
||||
self._attr_is_on = False
|
||||
|
||||
@@ -72,6 +72,13 @@ async def async_setup_entry(
|
||||
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_VALVE:
|
||||
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Cannot create Versatile Thermostat name=%s of type %s which is unknown",
|
||||
name,
|
||||
vt_type,
|
||||
)
|
||||
return
|
||||
|
||||
async_add_entities([entity], True)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
|
||||
@@ -99,34 +99,40 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
def _init_feature_flags(self, _):
|
||||
"""Fix features selection depending to infos"""
|
||||
is_empty: bool = False # TODO remove this not bool(infos)
|
||||
is_central_config = (
|
||||
self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
)
|
||||
|
||||
self._infos[CONF_USE_WINDOW_FEATURE] = (
|
||||
is_empty
|
||||
self._infos.get(CONF_USE_WINDOW_CENTRAL_CONFIG)
|
||||
or self._infos.get(CONF_WINDOW_SENSOR) is not None
|
||||
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
|
||||
)
|
||||
self._infos[CONF_USE_MOTION_FEATURE] = (
|
||||
is_empty
|
||||
or self._infos.get(CONF_MOTION_SENSOR) is not None
|
||||
or is_central_config
|
||||
)
|
||||
self._infos[CONF_USE_POWER_FEATURE] = is_empty or (
|
||||
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
|
||||
CONF_USE_MOTION_FEATURE, False
|
||||
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
|
||||
|
||||
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
|
||||
CONF_USE_POWER_CENTRAL_CONFIG, False
|
||||
) or (
|
||||
self._infos.get(CONF_POWER_SENSOR) is not None
|
||||
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
|
||||
)
|
||||
self._infos[CONF_USE_PRESENCE_FEATURE] = (
|
||||
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
||||
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
|
||||
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
||||
)
|
||||
|
||||
self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = is_empty or (
|
||||
self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = (
|
||||
self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV) is not None
|
||||
and self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV) is not None
|
||||
)
|
||||
|
||||
self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = (
|
||||
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True
|
||||
and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
)
|
||||
|
||||
def _init_central_config_flags(self, infos):
|
||||
"""Initialisation of central configuration flags"""
|
||||
is_empty: bool = not bool(infos)
|
||||
@@ -139,12 +145,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_MODE,
|
||||
):
|
||||
if not is_empty:
|
||||
current_config = self._infos.get(config, None)
|
||||
self._infos[config] = current_config is True or (
|
||||
current_config is None and self._central_config is not None
|
||||
|
||||
self._infos[config] = self._central_config is not None and (
|
||||
current_config is True or current_config is None
|
||||
)
|
||||
# self._infos[config] = current_config is True or (
|
||||
# current_config is None and self._central_config is not None
|
||||
# )
|
||||
else:
|
||||
self._infos[config] = self._central_config is not None
|
||||
|
||||
@@ -159,7 +170,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
# check the heater_entity_id
|
||||
for conf in [
|
||||
CONF_HEATER,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
@@ -167,15 +178,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_CLIMATE,
|
||||
]:
|
||||
d = data.get(conf, None) # pylint: disable=invalid-name
|
||||
if d is not None and self.hass.states.get(d) is None:
|
||||
_LOGGER.error(
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
|
||||
d,
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
if not isinstance(d, list):
|
||||
d = [d]
|
||||
for e in d:
|
||||
if e is not None and self.hass.states.get(e) is None:
|
||||
_LOGGER.error(
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
|
||||
e,
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
|
||||
# Check that only one window feature is used
|
||||
ws = self._infos.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name
|
||||
@@ -201,6 +214,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_MODE,
|
||||
# CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config
|
||||
CONF_USED_BY_CENTRAL_BOILER,
|
||||
]:
|
||||
if data.get(conf) is True:
|
||||
_LOGGER.error(
|
||||
@@ -242,6 +258,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
and infos.get(CONF_PRESENCE_SENSOR, None) is None
|
||||
):
|
||||
return False
|
||||
|
||||
if self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] and (
|
||||
not self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV, False)
|
||||
or len(self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)) <= 0
|
||||
or not self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, False)
|
||||
or len(self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)) <= 0
|
||||
):
|
||||
return False
|
||||
else:
|
||||
if (
|
||||
infos.get(CONF_NAME) is None
|
||||
@@ -256,21 +280,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH
|
||||
and infos.get(CONF_HEATER, None) is None
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
and infos.get(CONF_CLIMATE, None) is None
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
|
||||
and infos.get(CONF_VALVE, None) is None
|
||||
if infos.get(CONF_UNDERLYING_LIST, None) is not None and not infos.get(
|
||||
CONF_UNDERLYING_LIST, None
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -303,6 +314,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI
|
||||
and infos.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
|
||||
and (
|
||||
infos.get(CONF_TPI_COEF_INT, None) is None
|
||||
or infos.get(CONF_TPI_COEF_EXT) is None
|
||||
)
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False) is True
|
||||
and self._central_config is None
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
|
||||
@@ -422,6 +449,13 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if self._infos[CONF_USE_PRESENCE_FEATURE] is True:
|
||||
menu_options.append("presence")
|
||||
|
||||
if self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE) is True and self._infos[
|
||||
CONF_THERMOSTAT_TYPE
|
||||
] in [
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
]:
|
||||
menu_options.append("auto_start_stop")
|
||||
|
||||
menu_options.append("advanced")
|
||||
|
||||
if self.check_config_complete(self._infos):
|
||||
@@ -511,17 +545,29 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the Type flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_features user_input=%s", user_input)
|
||||
|
||||
schema = STEP_FEATURES_DATA_SCHEMA
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_FEATURES_DATA_SCHEMA
|
||||
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE:
|
||||
schema = STEP_CLIMATE_FEATURES_DATA_SCHEMA
|
||||
|
||||
return await self.generic_step(
|
||||
"features",
|
||||
(
|
||||
STEP_CENTRAL_FEATURES_DATA_SCHEMA
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
else STEP_FEATURES_DATA_SCHEMA
|
||||
),
|
||||
schema,
|
||||
user_input,
|
||||
self.async_step_menu,
|
||||
)
|
||||
|
||||
async def async_step_auto_start_stop(self, user_input: dict | None = None) -> FlowResult:
|
||||
""" Handle the Auto start stop step"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_auto_start_stop user_input=%s", user_input)
|
||||
|
||||
schema = STEP_AUTO_START_STOP
|
||||
self._infos[COMES_FROM] = None
|
||||
next_step = self.async_step_menu
|
||||
|
||||
return await self.generic_step("auto_start_stop", schema, user_input, next_step)
|
||||
|
||||
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the TPI flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
|
||||
@@ -857,6 +903,11 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
if not self._infos[CONF_USE_PRESENCE_FEATURE]:
|
||||
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
|
||||
self._infos[CONF_PRESENCE_SENSOR] = None
|
||||
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
|
||||
self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] = None
|
||||
self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] = None
|
||||
if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]:
|
||||
self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
_LOGGER.info(
|
||||
"Recreating entry %s due to configuration change. New config is now: %s",
|
||||
|
||||
@@ -16,6 +16,10 @@ from homeassistant.components.input_number import (
|
||||
DOMAIN as INPUT_NUMBER_DOMAIN,
|
||||
)
|
||||
|
||||
from homeassistant.components.input_datetime import (
|
||||
DOMAIN as INPUT_DATETIME_DOMAIN,
|
||||
)
|
||||
|
||||
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
|
||||
@@ -42,6 +46,11 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_LAST_SEEN_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SENSOR_DOMAIN, INPUT_DATETIME_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
||||
vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
@@ -59,6 +68,16 @@ STEP_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CLIMATE_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_AUTO_START_STOP_FEATURE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
@@ -100,17 +119,10 @@ STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
|
||||
|
||||
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_HEATER): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
@@ -125,17 +137,8 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_CLIMATE): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_CLIMATE_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_CLIMATE_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(
|
||||
@@ -164,17 +167,10 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_VALVE): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_VALVE_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_VALVE_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_VALVE_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
[
|
||||
@@ -187,6 +183,20 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_AUTO_START_STOP_LEVEL, default=AUTO_START_STOP_LEVEL_NONE
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_START_STOP_LEVELS,
|
||||
translation_key="auto_start_stop",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""Constants for the Versatile Thermostat integration."""
|
||||
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from enum import Enum
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
@@ -22,8 +23,8 @@ from .prop_algorithm import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_VERSION = 1
|
||||
CONFIG_MINOR_VERSION = 2
|
||||
CONFIG_VERSION = 2
|
||||
CONFIG_MINOR_VERSION = 0
|
||||
|
||||
PRESET_TEMP_SUFFIX = "_temp"
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
@@ -51,14 +52,13 @@ PLATFORMS: list[Platform] = [
|
||||
# Number should be after CLIMATE
|
||||
Platform.NUMBER,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_UNDERLYING_LIST = "underlying_entity_ids"
|
||||
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_LAST_SEEN_TEMP_SENSOR = "last_seen_temperature_sensor_entity_id"
|
||||
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
|
||||
CONF_POWER_SENSOR = "power_sensor_entity_id"
|
||||
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
|
||||
@@ -87,23 +87,16 @@ CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
|
||||
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
|
||||
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
|
||||
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
|
||||
CONF_CLIMATE = "climate_entity_id"
|
||||
CONF_CLIMATE_2 = "climate_entity2_id"
|
||||
CONF_CLIMATE_3 = "climate_entity3_id"
|
||||
CONF_CLIMATE_4 = "climate_entity4_id"
|
||||
CONF_USE_WINDOW_FEATURE = "use_window_feature"
|
||||
CONF_USE_MOTION_FEATURE = "use_motion_feature"
|
||||
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
|
||||
CONF_USE_POWER_FEATURE = "use_power_feature"
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
|
||||
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
|
||||
CONF_AC_MODE = "ac_mode"
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
|
||||
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
|
||||
CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
|
||||
@@ -123,9 +116,24 @@ CONF_AUTO_FAN_HIGH = "auto_fan_high"
|
||||
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
|
||||
CONF_STEP_TEMPERATURE = "step_temperature"
|
||||
|
||||
# Deprecated
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_CLIMATE = "climate_entity_id"
|
||||
CONF_CLIMATE_2 = "climate_entity2_id"
|
||||
CONF_CLIMATE_3 = "climate_entity3_id"
|
||||
CONF_CLIMATE_4 = "climate_entity4_id"
|
||||
CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
CONF_SAFETY_MODE = "safety_mode"
|
||||
CONF_MAX_ON_PERCENT = "max_on_percent"
|
||||
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
|
||||
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
|
||||
@@ -144,6 +152,36 @@ CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
|
||||
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
|
||||
CONF_WINDOW_ACTION = "window_action"
|
||||
|
||||
CONF_AUTO_START_STOP_LEVEL = "auto_start_stop_level"
|
||||
AUTO_START_STOP_LEVEL_NONE = "auto_start_stop_none"
|
||||
AUTO_START_STOP_LEVEL_SLOW = "auto_start_stop_slow"
|
||||
AUTO_START_STOP_LEVEL_MEDIUM = "auto_start_stop_medium"
|
||||
AUTO_START_STOP_LEVEL_FAST = "auto_start_stop_fast"
|
||||
CONF_AUTO_START_STOP_LEVELS = [
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
]
|
||||
|
||||
# For explicit typing purpose only
|
||||
TYPE_AUTO_START_STOP_LEVELS = Literal[ # pylint: disable=invalid-name
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
]
|
||||
|
||||
HVAC_OFF_REASON_NAME = "hvac_off_reason"
|
||||
HVAC_OFF_REASON_MANUAL = "manual"
|
||||
HVAC_OFF_REASON_AUTO_START_STOP = "auto_start_stop"
|
||||
HVAC_OFF_REASON_WINDOW_DETECTION = "window_detection"
|
||||
HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name
|
||||
HVAC_OFF_REASON_MANUAL,
|
||||
HVAC_OFF_REASON_AUTO_START_STOP,
|
||||
HVAC_OFF_REASON_WINDOW_DETECTION,
|
||||
]
|
||||
|
||||
DEFAULT_SHORT_EMA_PARAMS = {
|
||||
"max_alpha": 0.5,
|
||||
# In sec
|
||||
@@ -215,10 +253,6 @@ CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
|
||||
ALL_CONF = (
|
||||
[
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
@@ -248,20 +282,12 @@ ALL_CONF = (
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_USE_MOTION_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AC_MODE,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
@@ -329,7 +355,11 @@ CONF_WINDOW_ACTIONS = [
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||
@@ -444,6 +474,7 @@ class EventType(Enum):
|
||||
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
|
||||
PRESET_EVENT: str = "versatile_thermostat_preset_event"
|
||||
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
|
||||
AUTO_START_STOP_EVENT: str = "versatile_thermostat_auto_start_stop_event"
|
||||
|
||||
|
||||
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
|
||||
|
||||
18
custom_components/versatile_thermostat/icons.json
Normal file
18
custom_components/versatile_thermostat/icons.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"shedding": "mdi:power-plug-off",
|
||||
"safety": "mdi:shield-alert",
|
||||
"none": "mdi:knob",
|
||||
"frost": "mdi:snowflake"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ the keep_alive setting of Home Assistant's Generic Thermostat integration:
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta, datetime
|
||||
from time import monotonic
|
||||
|
||||
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -18,16 +19,95 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackoffTimer:
|
||||
"""Exponential backoff timer with a non-blocking polling-style implementation.
|
||||
|
||||
Usage example:
|
||||
timer = BackoffTimer(multiplier=1.5, upper_limit_sec=600)
|
||||
while some_condition:
|
||||
if timer.is_ready():
|
||||
do_something()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
multiplier=2.0,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=True,
|
||||
):
|
||||
"""Initialize a BackoffTimer instance.
|
||||
|
||||
Args:
|
||||
multiplier (int, optional): Period multiplier applied when is_ready() is True.
|
||||
lower_limit_sec (int, optional): Initial backoff period in seconds.
|
||||
upper_limit_sec (int, optional): Maximum backoff period in seconds.
|
||||
initially_ready (bool, optional): Whether is_ready() should return True the
|
||||
first time it is called, or after a call to reset().
|
||||
"""
|
||||
self._multiplier = multiplier
|
||||
self._lower_limit_sec = lower_limit_sec
|
||||
self._upper_limit_sec = upper_limit_sec
|
||||
self._initially_ready = initially_ready
|
||||
|
||||
self._timestamp = 0
|
||||
self._period_sec = self._lower_limit_sec
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool:
|
||||
"""Whether the backoff timer is in progress (True after a call to is_ready())."""
|
||||
return bool(self._timestamp)
|
||||
|
||||
def reset(self):
|
||||
"""Reset a BackoffTimer instance."""
|
||||
self._timestamp = 0
|
||||
self._period_sec = self._lower_limit_sec
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
"""Check whether an exponentially increasing period of time has passed.
|
||||
|
||||
Whenever is_ready() returns True, the timer period is multiplied so that
|
||||
it takes longer until is_ready() returns True again.
|
||||
Returns:
|
||||
bool: True if enough time has passed since one of the following events,
|
||||
in relation to an instance of this class:
|
||||
- The last time when this method returned True, if it ever did.
|
||||
- Or else, when this method was first called after a call to reset().
|
||||
- Or else, when this method was first called.
|
||||
False otherwise.
|
||||
"""
|
||||
now = monotonic()
|
||||
if self._timestamp == 0:
|
||||
self._timestamp = now
|
||||
return self._initially_ready
|
||||
elif now - self._timestamp >= self._period_sec:
|
||||
self._timestamp = now
|
||||
self._period_sec = max(
|
||||
self._lower_limit_sec,
|
||||
min(self._upper_limit_sec, self._period_sec * self._multiplier),
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class IntervalCaller:
|
||||
"""Repeatedly call a given async action function at a given regular interval.
|
||||
|
||||
Convenience wrapper around Home Assistant's `async_track_time_interval` function.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, interval_sec: int) -> None:
|
||||
def __init__(self, hass: HomeAssistant, interval_sec: float) -> None:
|
||||
self._hass = hass
|
||||
self._interval_sec = interval_sec
|
||||
self._remove_handle: CALLBACK_TYPE | None = None
|
||||
self.backoff_timer = BackoffTimer()
|
||||
|
||||
@property
|
||||
def interval_sec(self) -> float:
|
||||
"""Return the calling interval in seconds."""
|
||||
return self._interval_sec
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel the regular calls to the action function."""
|
||||
@@ -43,7 +123,11 @@ class IntervalCaller:
|
||||
|
||||
async def callback(_time: datetime):
|
||||
try:
|
||||
_LOGGER.debug("Calling keep-alive action")
|
||||
_LOGGER.debug(
|
||||
"Calling keep-alive action '%s' (%ss interval)",
|
||||
action.__name__,
|
||||
self._interval_sec,
|
||||
)
|
||||
await action()
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error(e)
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.0.2",
|
||||
"version": "6.7.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -11,13 +11,15 @@ from homeassistant.components.number import (
|
||||
NumberMode,
|
||||
NumberDeviceClass,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
DEFAULT_STEP,
|
||||
)
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
)
|
||||
from homeassistant.components.sensor import UnitOfTemperature
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -51,7 +53,9 @@ from .const import (
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
overrides,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG,
|
||||
)
|
||||
|
||||
PRESET_ICON_MAPPING = {
|
||||
@@ -153,9 +157,10 @@ async def async_setup_entry(
|
||||
|
||||
# For central config only
|
||||
else:
|
||||
entities.append(
|
||||
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
|
||||
)
|
||||
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
|
||||
entities.append(
|
||||
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
|
||||
)
|
||||
for preset in CONF_PRESETS_WITH_AC_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number central, AC, non AWAY for preset %s",
|
||||
@@ -178,7 +183,8 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
if len(entities) > 0:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class ActivateBoilerThresholdNumber(
|
||||
@@ -276,7 +282,7 @@ class CentralConfigTemperatureNumber(
|
||||
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
|
||||
self._attr_unique_id = f"central_configuration_preset_{preset_name}"
|
||||
self._attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
|
||||
|
||||
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
|
||||
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
|
||||
@@ -338,7 +344,10 @@ class CentralConfigTemperatureNumber(
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""The value have change from the Number Entity in UI"""
|
||||
float_value = float(value)
|
||||
old_value = float(self._attr_native_value)
|
||||
old_value = (
|
||||
None if self._attr_native_value is None else float(self._attr_native_value)
|
||||
)
|
||||
|
||||
if float_value == old_value:
|
||||
return
|
||||
|
||||
@@ -350,7 +359,7 @@ class CentralConfigTemperatureNumber(
|
||||
# We have to reload all VTherm for which uses the central configuration
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
# Update the VTherms which have temperature in central config
|
||||
self.hass.create_task(api.init_vtherm_links(only_use_central=True))
|
||||
self.hass.create_task(api.init_vtherm_preset_with_central())
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
@@ -361,7 +370,7 @@ class CentralConfigTemperatureNumber(
|
||||
# TODO Kelvin ? It seems not because all internal values are stored in
|
||||
# ° Celsius but only the render in front can be in °K depending on the
|
||||
# user configuration.
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
|
||||
class TemperatureNumber( # pylint: disable=abstract-method
|
||||
@@ -390,11 +399,13 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
|
||||
self._attr_unique_id = f"{self._device_name}_preset_{preset_name}"
|
||||
self._attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
|
||||
|
||||
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
|
||||
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
|
||||
self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX)
|
||||
self._has_central_main_attributes = entry_infos.get(
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG, False
|
||||
)
|
||||
|
||||
self.init_min_max_step(entry_infos)
|
||||
|
||||
# Initialize the values if included into the entry_infos. This will do
|
||||
# the temperature migration.
|
||||
@@ -456,7 +467,9 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
return
|
||||
|
||||
float_value = float(value)
|
||||
old_value = float(self._attr_native_value)
|
||||
old_value = (
|
||||
None if self._attr_native_value is None else float(self._attr_native_value)
|
||||
)
|
||||
|
||||
if float_value == old_value:
|
||||
return
|
||||
@@ -473,6 +486,10 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
)
|
||||
)
|
||||
|
||||
# We set the min, max and step from central config if relevant because it is possible
|
||||
# that central config was not loaded at startup
|
||||
self.init_min_max_step()
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -480,5 +497,28 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""The unit of measurement"""
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
def init_min_max_step(self, entry_infos=None):
|
||||
"""Initialize min, max and step value from config or from central config"""
|
||||
if self._has_central_main_attributes:
|
||||
vthermapi: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
central_config = vthermapi.find_central_configuration()
|
||||
if central_config:
|
||||
self._attr_native_step = central_config.data.get(CONF_STEP_TEMPERATURE)
|
||||
self._attr_native_min_value = central_config.data.get(CONF_TEMP_MIN)
|
||||
self._attr_native_max_value = central_config.data.get(CONF_TEMP_MAX)
|
||||
|
||||
return
|
||||
|
||||
if entry_infos:
|
||||
self._attr_native_step = entry_infos.get(
|
||||
CONF_STEP_TEMPERATURE, DEFAULT_STEP
|
||||
)
|
||||
self._attr_native_min_value = entry_infos.get(
|
||||
CONF_TEMP_MIN, DEFAULT_MIN_VALUE
|
||||
)
|
||||
self._attr_native_max_value = entry_infos.get(
|
||||
CONF_TEMP_MAX, DEFAULT_MAX_VALUE
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" The TPI calculation module """
|
||||
# pylint: disable='line-too-long'
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
@@ -14,6 +15,11 @@ PROPORTIONAL_MIN_DURATION_SEC = 10
|
||||
FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
|
||||
|
||||
|
||||
def is_number(value):
|
||||
"""check if value is a number"""
|
||||
return isinstance(value, (int, float))
|
||||
|
||||
|
||||
class PropAlgorithm:
|
||||
"""This class aims to do all calculation of the Proportional alogorithm"""
|
||||
|
||||
@@ -24,16 +30,44 @@ class PropAlgorithm:
|
||||
tpi_coef_ext,
|
||||
cycle_min: int,
|
||||
minimal_activation_delay: int,
|
||||
vtherm_entity_id: str = None,
|
||||
max_on_percent: float = None,
|
||||
) -> None:
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # pylint: disable=line-too-long
|
||||
"%s - Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # pylint: disable=line-too-long
|
||||
vtherm_entity_id,
|
||||
function_type,
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
cycle_min,
|
||||
minimal_activation_delay,
|
||||
)
|
||||
|
||||
# Issue 506 - check parameters
|
||||
if (
|
||||
vtherm_entity_id is None
|
||||
or not is_number(tpi_coef_int)
|
||||
or not is_number(tpi_coef_ext)
|
||||
or not is_number(cycle_min)
|
||||
or not is_number(minimal_activation_delay)
|
||||
or function_type != PROPORTIONAL_FUNCTION_TPI
|
||||
):
|
||||
_LOGGER.error(
|
||||
"%s - configuration is wrong. function_type=%s, entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s, cycle_min is %s, minimal_activation_delay is %s",
|
||||
vtherm_entity_id,
|
||||
function_type,
|
||||
vtherm_entity_id,
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
cycle_min,
|
||||
minimal_activation_delay,
|
||||
)
|
||||
raise TypeError(
|
||||
"TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values"
|
||||
)
|
||||
|
||||
self._vtherm_entity_id = vtherm_entity_id
|
||||
self._function = function_type
|
||||
self._tpi_coef_int = tpi_coef_int
|
||||
self._tpi_coef_ext = tpi_coef_ext
|
||||
@@ -45,6 +79,7 @@ class PropAlgorithm:
|
||||
self._off_time_sec = self._cycle_min * 60
|
||||
self._security = False
|
||||
self._default_on_percent = 0
|
||||
self._max_on_percent = max_on_percent
|
||||
|
||||
def calculate(
|
||||
self,
|
||||
@@ -57,16 +92,19 @@ class PropAlgorithm:
|
||||
if target_temp is None or current_temp is None:
|
||||
log = _LOGGER.debug if hvac_mode == HVACMode.OFF else _LOGGER.warning
|
||||
log(
|
||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
|
||||
"%s - Proportional algorithm: calculation is not possible cause target_temp (%s) or current_temp (%s) is null. Heating/cooling will be disabled. This could be normal at startup", # pylint: disable=line-too-long
|
||||
self._vtherm_entity_id,
|
||||
target_temp,
|
||||
current_temp,
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
else:
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
delta_temp = current_temp - target_temp
|
||||
delta_ext_temp = (
|
||||
ext_current_temp
|
||||
ext_current_temp - target_temp
|
||||
if ext_current_temp is not None
|
||||
else 0 - target_temp
|
||||
else 0
|
||||
)
|
||||
else:
|
||||
delta_temp = target_temp - current_temp
|
||||
@@ -83,7 +121,8 @@ class PropAlgorithm:
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
"%s - Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
self._vtherm_entity_id,
|
||||
self._function,
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
@@ -91,7 +130,8 @@ class PropAlgorithm:
|
||||
self._calculate_internal()
|
||||
|
||||
_LOGGER.debug(
|
||||
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
|
||||
"%s - heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
|
||||
self._vtherm_entity_id,
|
||||
current_temp if current_temp else -9999.0,
|
||||
ext_current_temp if ext_current_temp else -9999.0,
|
||||
target_temp if target_temp else -9999.0,
|
||||
@@ -110,11 +150,12 @@ class PropAlgorithm:
|
||||
self._calculated_on_percent = 0
|
||||
|
||||
if self._security:
|
||||
_LOGGER.debug(
|
||||
"Security is On using the default_on_percent %f",
|
||||
self._default_on_percent,
|
||||
)
|
||||
self._on_percent = self._default_on_percent
|
||||
_LOGGER.info(
|
||||
"%s - Security is On using the default_on_percent %f",
|
||||
self._vtherm_entity_id,
|
||||
self._on_percent,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Security is Off using the calculated_on_percent %f",
|
||||
@@ -122,19 +163,23 @@ class PropAlgorithm:
|
||||
)
|
||||
self._on_percent = self._calculated_on_percent
|
||||
|
||||
if self._max_on_percent is not None and self._on_percent > self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"%s - Heating period clamped to %s (instead of %s) due to max_on_percent setting.",
|
||||
self._vtherm_entity_id,
|
||||
self._max_on_percent,
|
||||
self._on_percent,
|
||||
)
|
||||
self._on_percent = self._max_on_percent
|
||||
|
||||
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
if self._on_time_sec < self._minimal_activation_delay:
|
||||
if self._on_time_sec > 0:
|
||||
_LOGGER.info(
|
||||
"No heating period due to heating period too small (%f < %f)",
|
||||
self._on_time_sec,
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"No heating period due to heating period too small (%f < %f)",
|
||||
"%s - No heating period due to heating period too small (%f < %f)",
|
||||
self._vtherm_entity_id,
|
||||
self._on_time_sec,
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
@@ -144,12 +189,18 @@ class PropAlgorithm:
|
||||
|
||||
def set_security(self, default_on_percent: float):
|
||||
"""Set a default value for on_percent (used for safety mode)"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
|
||||
)
|
||||
self._security = True
|
||||
self._default_on_percent = default_on_percent
|
||||
self._calculate_internal()
|
||||
|
||||
def unset_security(self):
|
||||
"""Unset the safety mode"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id
|
||||
)
|
||||
self._security = False
|
||||
self._calculate_internal()
|
||||
|
||||
|
||||
@@ -3,21 +3,20 @@
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import (
|
||||
BaseThermostat,
|
||||
ConfigData,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
@@ -96,17 +95,20 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
if old_state is not None:
|
||||
self._attr_current_option = old_state.state
|
||||
|
||||
@callback
|
||||
async def _async_startup_internal(*_):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
await self.notify_central_mode_change()
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
api.register_central_mode_select(self)
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
await _async_startup_internal()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
)
|
||||
# @callback
|
||||
# async def _async_startup_internal(*_):
|
||||
# _LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
# await self.notify_central_mode_change()
|
||||
#
|
||||
# if self.hass.state == CoreState.running:
|
||||
# await _async_startup_internal()
|
||||
# else:
|
||||
# self.hass.bus.async_listen_once(
|
||||
# EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
# )
|
||||
|
||||
@overrides
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
@@ -120,19 +122,17 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option = option
|
||||
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||
|
||||
@overrides
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option"""
|
||||
# Update the VTherms which have temperature in central config
|
||||
self.hass.create_task(self.async_select_option(option))
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
# Update all VTherm states
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if isinstance(entity, BaseThermostat):
|
||||
_LOGGER.debug(
|
||||
"Changing the central_mode. We have find %s to update",
|
||||
entity.name,
|
||||
)
|
||||
await entity.check_central_mode(
|
||||
self._attr_current_option, old_central_mode
|
||||
)
|
||||
await api.notify_central_mode_change(old_central_mode)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -79,7 +79,6 @@ async def async_setup_entry(
|
||||
entities = [
|
||||
NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data)
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
else:
|
||||
entities = [
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
@@ -108,6 +107,7 @@ async def async_setup_entry(
|
||||
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -125,15 +125,15 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(self.my_climate.total_energy) or math.isinf(
|
||||
self.my_climate.total_energy
|
||||
):
|
||||
energy = self.my_climate.total_energy
|
||||
if energy is None:
|
||||
return
|
||||
|
||||
if math.isnan(energy) or math.isinf(energy):
|
||||
raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.total_energy, self.suggested_display_precision
|
||||
)
|
||||
self._attr_native_value = round(energy, self.suggested_display_precision)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -570,7 +570,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
@@ -621,7 +621,7 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -37,7 +38,8 @@
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
@@ -49,6 +51,8 @@
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -60,57 +64,36 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -259,6 +242,7 @@
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -269,7 +253,8 @@
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
@@ -281,6 +266,8 @@
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -292,57 +279,36 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -508,6 +474,14 @@
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -518,7 +492,8 @@
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Safety",
|
||||
"none": "Manual"
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,4 +547,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
168
custom_components/versatile_thermostat/switch.py
Normal file
168
custom_components/versatile_thermostat/switch.py
Normal file
@@ -0,0 +1,168 @@
|
||||
## pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
|
||||
from .const import * # pylint: disable=unused-wildcard-import,wildcard-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat switches with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
||||
|
||||
entities = []
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
|
||||
|
||||
if auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
entities.append(enable_entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
||||
"""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 = "Enable auto start/stop"
|
||||
self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
|
||||
self._default_value = (
|
||||
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
|
||||
!= AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:power-sleep"
|
||||
|
||||
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 = self._default_value
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
def update_my_state_and_vtherm(self):
|
||||
"""Update the auto_start_stop_enable flag in my VTherm"""
|
||||
self.async_write_ha_state()
|
||||
if self.my_climate is not None:
|
||||
self.my_climate.set_auto_start_stop_enable(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()
|
||||
|
||||
|
||||
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()
|
||||
@@ -3,13 +3,13 @@
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
@@ -20,45 +20,30 @@ from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
from .const import (
|
||||
overrides,
|
||||
DOMAIN,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
|
||||
CONF_AUTO_FAN_MODE,
|
||||
CONF_AUTO_FAN_NONE,
|
||||
CONF_AUTO_FAN_LOW,
|
||||
CONF_AUTO_FAN_MEDIUM,
|
||||
CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_FAN_TURBO,
|
||||
RegulationParamSlow,
|
||||
RegulationParamLight,
|
||||
RegulationParamMedium,
|
||||
RegulationParamStrong,
|
||||
AUTO_FAN_DTEMP_THRESHOLD,
|
||||
AUTO_FAN_DEACTIVATED_MODES,
|
||||
UnknownEntity,
|
||||
)
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .underlyings import UnderlyingClimate
|
||||
from .auto_start_stop_algorithm import (
|
||||
AutoStartStopDetectionAlgorithm,
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_ENERGY = logging.getLogger(
|
||||
"custom_components.versatile_thermostat.energy_debug"
|
||||
)
|
||||
|
||||
|
||||
class ThermostatOverClimate(BaseThermostat):
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
HVACAction.COOLING,
|
||||
HVACAction.DRYING,
|
||||
HVACAction.FAN,
|
||||
HVACAction.HEATING,
|
||||
]
|
||||
|
||||
class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
|
||||
_auto_regulation_mode: str | None = None
|
||||
@@ -74,6 +59,10 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
# The fan_mode name depending of the current_mode
|
||||
_auto_activated_fan_mode: str | None = None
|
||||
_auto_deactivated_fan_mode: str | None = None
|
||||
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
|
||||
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
_is_auto_start_stop_enabled: bool = False
|
||||
_follow_underlying_temp_change: bool = False
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
@@ -81,10 +70,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
{
|
||||
"is_over_climate",
|
||||
"start_hvac_action_date",
|
||||
"underlying_climate_0",
|
||||
"underlying_climate_1",
|
||||
"underlying_climate_2",
|
||||
"underlying_climate_3",
|
||||
"underlying_entities",
|
||||
"regulation_accumulated_error",
|
||||
"auto_regulation_mode",
|
||||
"auto_fan_mode",
|
||||
@@ -92,6 +78,12 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
"auto_activated_fan_mode",
|
||||
"auto_deactivated_fan_mode",
|
||||
"auto_regulation_use_device_temp",
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"follow_underlying_temp_change",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -106,6 +98,61 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
self._last_regulation_change = NowClass.get_now(hass)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=climate,
|
||||
)
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
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 5
|
||||
)
|
||||
|
||||
self._auto_fan_mode = (
|
||||
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_use_device_temp = config_entry.get(
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
|
||||
if use_auto_start_stop:
|
||||
self._auto_start_stop_level = config_entry.get(
|
||||
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
else:
|
||||
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
# Instanciate the auto start stop algo
|
||||
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
|
||||
self._auto_start_stop_level, self.name
|
||||
)
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
"""True if the Thermostat is over_climate"""
|
||||
@@ -143,7 +190,17 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ")
|
||||
_LOGGER.debug(
|
||||
"%s - don't send regulated temperature cause VTherm is off ", self
|
||||
)
|
||||
return
|
||||
|
||||
if self.target_temperature is None:
|
||||
_LOGGER.warning(
|
||||
"%s - don't send regulated temperature cause VTherm target_temp (%s) is None. This should be a temporary warning message.",
|
||||
self,
|
||||
self.target_temperature,
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
@@ -168,12 +225,19 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
_LOGGER.info("%s - regulation calculation will be done", self)
|
||||
|
||||
new_regulated_temp = round_to_nearest(
|
||||
self._regulation_algo.calculate_regulated_temperature(
|
||||
self.current_temperature, self._cur_ext_temp
|
||||
),
|
||||
self._auto_regulation_dtemp,
|
||||
)
|
||||
# use _attr_target_temperature_step to round value if _auto_regulation_dtemp is equal to 0
|
||||
regulation_step = self._auto_regulation_dtemp if self._auto_regulation_dtemp else self._attr_target_temperature_step
|
||||
_LOGGER.debug("%s - usage regulation_step: %.2f ", self, regulation_step)
|
||||
|
||||
if self.current_temperature is not None:
|
||||
new_regulated_temp = round_to_nearest(
|
||||
self._regulation_algo.calculate_regulated_temperature(
|
||||
self.current_temperature, self._cur_ext_temp
|
||||
),
|
||||
regulation_step,
|
||||
)
|
||||
else:
|
||||
new_regulated_temp = self.target_temperature
|
||||
dtemp = new_regulated_temp - self._regulated_target_temp
|
||||
|
||||
if not force and abs(dtemp) < self._auto_regulation_dtemp:
|
||||
@@ -198,25 +262,16 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
offset_temp = 0
|
||||
device_temp = 0
|
||||
if (
|
||||
# current_temperature is set
|
||||
self.current_temperature is not None
|
||||
# regulation can use the device_temp
|
||||
self.auto_regulation_use_device_temp
|
||||
and self.auto_regulation_use_device_temp
|
||||
# and we have access to the device temp
|
||||
and (device_temp := under.underlying_current_temperature) is not None
|
||||
# and target is not reach (ie we need regulation)
|
||||
and (
|
||||
(
|
||||
self.hvac_mode == HVACMode.COOL
|
||||
and self.target_temperature < self.current_temperature
|
||||
)
|
||||
or (
|
||||
self.hvac_mode == HVACMode.HEAT
|
||||
and self.target_temperature > self.current_temperature
|
||||
)
|
||||
)
|
||||
):
|
||||
offset_temp = device_temp - self.current_temperature
|
||||
|
||||
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, self._auto_regulation_dtemp)
|
||||
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, regulation_step)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f",
|
||||
@@ -277,53 +332,6 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
for climate in [
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
]:
|
||||
if config_entry.get(climate):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=config_entry.get(climate),
|
||||
)
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
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 5
|
||||
)
|
||||
|
||||
self._auto_fan_mode = (
|
||||
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_use_device_temp = config_entry.get(
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
@@ -494,18 +502,10 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._attr_extra_state_attributes["start_hvac_action_date"] = (
|
||||
self._underlying_climate_start_hvac_action_date
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_climate_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
if self.is_regulated:
|
||||
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
|
||||
@@ -536,7 +536,30 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self.auto_regulation_use_device_temp
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_enable"] = (
|
||||
self.auto_start_stop_enable
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_level"] = (
|
||||
self._auto_start_stop_algo.level
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_start_stop_dtmin"] = (
|
||||
self._auto_start_stop_algo.dt_min
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = (
|
||||
self._auto_start_stop_algo.accumulated_error
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_start_stop_accumulated_error_threshold"
|
||||
] = self._auto_start_stop_algo.accumulated_error_threshold
|
||||
|
||||
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
|
||||
self._follow_underlying_temp_change
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
@@ -581,7 +604,21 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
):
|
||||
added_energy = self._device_power * self._underlying_climate_delta_t
|
||||
|
||||
self._total_energy += added_energy
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy incremented energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
@@ -590,7 +627,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_climate_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
async def _async_climate_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle unerdlying climate state changes.
|
||||
This method takes the underlying values and update the VTherm with them.
|
||||
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
|
||||
@@ -603,7 +640,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
async def end_climate_changed(changes: bool):
|
||||
"""To end the event management"""
|
||||
if changes:
|
||||
self.async_write_ha_state()
|
||||
# already done by update_custom_attribute
|
||||
# self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
await self.async_control_heating()
|
||||
|
||||
@@ -612,6 +650,15 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
# Find the underlying which have change
|
||||
under = self.find_underlying_by_entity_id(new_state.entity_id)
|
||||
|
||||
if not under:
|
||||
_LOGGER.warning(
|
||||
"We have a receive an event from entity %s which is NOT one of our underlying entities. This is not normal and should be reported to the developper of the integration"
|
||||
)
|
||||
return
|
||||
|
||||
changes = False
|
||||
new_hvac_mode = new_state.state
|
||||
|
||||
@@ -646,20 +693,68 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
new_state.last_updated if new_state and new_state.last_updated else None
|
||||
)
|
||||
|
||||
new_target_temp = (
|
||||
new_state.attributes.get("temperature")
|
||||
if new_state and new_state.attributes
|
||||
else None
|
||||
)
|
||||
|
||||
last_sent_temperature = under.last_sent_temperature or 0
|
||||
under_temp_diff = (
|
||||
(new_target_temp - last_sent_temperature) if new_target_temp else 0
|
||||
)
|
||||
if -1 < under_temp_diff < 1:
|
||||
under_temp_diff = 0
|
||||
|
||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
||||
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
||||
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
||||
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
||||
# new_hvac_mode = HVACMode.OFF
|
||||
|
||||
# Forget event when the event holds no real changes
|
||||
if (
|
||||
new_hvac_mode == self._hvac_mode
|
||||
and new_hvac_action == old_hvac_action
|
||||
and under_temp_diff == 0
|
||||
and (new_fan_mode is None or new_fan_mode == self._attr_fan_mode)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - a underlying state change event is received but no real change have been found. Forget the event",
|
||||
self,
|
||||
)
|
||||
return
|
||||
|
||||
# Ignore new target temperature when out of range
|
||||
if (
|
||||
not new_target_temp is None
|
||||
and not self._attr_min_temp is None
|
||||
and not self._attr_max_temp is None
|
||||
and not (self._attr_min_temp <= new_target_temp <= self._attr_max_temp)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - underlying sent a target temperature (%s) which is out of configured min/max range (%s / %s). The value will be ignored",
|
||||
self,
|
||||
new_target_temp,
|
||||
self._attr_min_temp,
|
||||
self._attr_max_temp,
|
||||
)
|
||||
new_target_temp = None
|
||||
under_temp_diff = 0
|
||||
|
||||
# A real changes have to be managed
|
||||
_LOGGER.info(
|
||||
"%s - Underlying climate %s changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
||||
"%s - Underlying climate %s have changed. new_hvac_mode is %s (vs %s), new_hvac_action=%s (vs %s), new_target_temp=%s (vs %s), new_fan_mode=%s (vs %s)",
|
||||
self,
|
||||
new_state.entity_id,
|
||||
under.entity_id,
|
||||
new_hvac_mode,
|
||||
self._hvac_mode,
|
||||
new_hvac_action,
|
||||
old_hvac_action,
|
||||
new_target_temp,
|
||||
self.target_temperature,
|
||||
new_fan_mode,
|
||||
self._attr_fan_mode,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -673,12 +768,6 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
# Interpretation of hvac action
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
HVACAction.COOLING,
|
||||
HVACAction.DRYING,
|
||||
HVACAction.FAN,
|
||||
HVACAction.HEATING,
|
||||
]
|
||||
if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON:
|
||||
self._underlying_climate_start_hvac_action_date = (
|
||||
self.get_last_updated_date_or_now(new_state)
|
||||
@@ -711,6 +800,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
changes = True
|
||||
|
||||
# Filter new state when received just after a change from VTherm
|
||||
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
|
||||
# In that case a loop is possible if a user change multiple times during this 6 sec.
|
||||
if new_state_date_updated and self._last_change_time:
|
||||
@@ -723,6 +813,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await end_climate_changed(changes)
|
||||
return
|
||||
|
||||
# Update all underlyings hvac_mode state if it has change
|
||||
if (
|
||||
new_hvac_mode
|
||||
in [
|
||||
@@ -737,7 +828,6 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
]
|
||||
and self._hvac_mode != new_hvac_mode
|
||||
):
|
||||
# Update all underlyings state
|
||||
# Issue #334 - if all underlyings are not aligned with the same hvac_mode don't change the underlying and wait they are aligned
|
||||
if self.is_over_climate:
|
||||
for under in self._underlyings:
|
||||
@@ -768,35 +858,119 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._attr_fan_mode = new_fan_mode
|
||||
changes = True
|
||||
|
||||
if not changes:
|
||||
# try to manage new target temperature set if state
|
||||
# try to manage new target temperature set if state if no other changes have been found
|
||||
# and if a target temperature have already been sent
|
||||
if (
|
||||
self._follow_underlying_temp_change
|
||||
and not changes
|
||||
and under.last_sent_temperature is not None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Do temperature check. temperature is %s, new_state.attributes is %s",
|
||||
self.target_temperature,
|
||||
new_state.attributes,
|
||||
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
||||
under.last_sent_temperature,
|
||||
new_target_temp,
|
||||
)
|
||||
if (
|
||||
# we do not change target temperature on regulated VTherm
|
||||
not self.is_regulated
|
||||
and new_state.attributes
|
||||
and (new_target_temp := new_state.attributes.get("temperature"))
|
||||
and new_target_temp != self.target_temperature
|
||||
):
|
||||
# if the underlying have change its target temperature
|
||||
if under_temp_diff != 0:
|
||||
_LOGGER.info(
|
||||
"%s - Target temp in underlying have change to %s",
|
||||
"%s - Target temp in underlying have change to %s (vs %s)",
|
||||
self,
|
||||
new_target_temp,
|
||||
under.last_sent_temperature,
|
||||
)
|
||||
await self.async_set_temperature(temperature=new_target_temp)
|
||||
changes = True
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - Forget the eventual underlying temperature change there is no real change",
|
||||
self,
|
||||
)
|
||||
|
||||
await end_climate_changed(changes)
|
||||
|
||||
async def check_auto_start_stop(self):
|
||||
"""Check the auto-start-stop and an eventual action
|
||||
Return False if we should stop the control_heating method"""
|
||||
slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
|
||||
action = self._auto_start_stop_algo.calculate_action(
|
||||
self.hvac_mode,
|
||||
self._saved_hvac_mode,
|
||||
self.target_temperature,
|
||||
self.current_temperature,
|
||||
slope,
|
||||
self.now,
|
||||
)
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
|
||||
self,
|
||||
)
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
|
||||
await self.async_turn_off()
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": self.name,
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"target_temperature": self.target_temperature,
|
||||
"current_temperature": self.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop here
|
||||
return False
|
||||
elif action == AUTO_START_STOP_ACTION_ON:
|
||||
_LOGGER.info(
|
||||
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
|
||||
)
|
||||
await self.async_turn_on()
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"target_temperature": self.target_temperature,
|
||||
"current_temperature": self.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
return True
|
||||
|
||||
@overrides
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
# Check if we need to auto start/stop the Vtherm
|
||||
if self.auto_start_stop_enable:
|
||||
continu = await self.check_auto_start_stop()
|
||||
if not continu:
|
||||
return ret
|
||||
else:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled")
|
||||
|
||||
# Continue the normal async_control_heating
|
||||
|
||||
# Send the regulated temperature to the underlyings
|
||||
await self._send_regulated_temperature()
|
||||
|
||||
if self._auto_fan_mode and self._auto_fan_mode != CONF_AUTO_FAN_NONE:
|
||||
@@ -804,6 +978,16 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
return ret
|
||||
|
||||
def set_auto_start_stop_enable(self, is_enabled: bool):
|
||||
"""Enable/Disable the auto-start/stop feature"""
|
||||
self._is_auto_start_stop_enabled = is_enabled
|
||||
self.update_custom_attributes()
|
||||
|
||||
def set_follow_underlying_temp_change(self, follow: bool):
|
||||
"""Set the flaf follow the underlying temperature changes"""
|
||||
self._follow_underlying_temp_change = follow
|
||||
self.update_custom_attributes()
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self) -> str | None:
|
||||
"""Get the regulation mode"""
|
||||
@@ -890,10 +1074,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).temperature_unit
|
||||
|
||||
return self._unit
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -953,6 +1134,21 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Return the auto start/stop level."""
|
||||
return self._auto_start_stop_level
|
||||
|
||||
@property
|
||||
def auto_start_stop_enable(self) -> bool:
|
||||
"""Returns the auto_start_stop_enable"""
|
||||
return self._is_auto_start_stop_enabled
|
||||
|
||||
@property
|
||||
def follow_underlying_temp_change(self) -> bool:
|
||||
"""Get the follow underlying temp change flag"""
|
||||
return self._follow_underlying_temp_change
|
||||
|
||||
@overrides
|
||||
def init_underlyings(self):
|
||||
"""Init the underlyings if not already done"""
|
||||
@@ -1084,3 +1280,29 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self.choose_auto_fan_mode(CONF_AUTO_FAN_TURBO)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
@overrides
|
||||
async def async_turn_off(self) -> None:
|
||||
# if window is open, don't overwrite the saved_hvac_mode
|
||||
if self.window_state != STATE_ON:
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
@overrides
|
||||
async def async_turn_on(self) -> None:
|
||||
|
||||
# don't turn_on if window is open
|
||||
if self.window_state == STATE_ON:
|
||||
_LOGGER.info(
|
||||
"%s - refuse to turn on because window is open. We keep the save_hvac_mode",
|
||||
self,
|
||||
)
|
||||
return
|
||||
|
||||
if self._saved_hvac_mode is not None: # pylint: disable=protected-access
|
||||
await self.restore_hvac_mode(True)
|
||||
else:
|
||||
if self._ac_mode:
|
||||
await self.async_set_hvac_mode(HVACMode.COOL)
|
||||
else:
|
||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_INVERSE_SWITCH,
|
||||
overrides,
|
||||
@@ -25,9 +21,11 @@ from .underlyings import UnderlyingSwitch
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_ENERGY = logging.getLogger(
|
||||
"custom_components.versatile_thermostat.energy_debug"
|
||||
)
|
||||
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat):
|
||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
@@ -36,10 +34,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
{
|
||||
"is_over_switch",
|
||||
"is_inversed",
|
||||
"underlying_switch_0",
|
||||
"underlying_switch_1",
|
||||
"underlying_switch_2",
|
||||
"underlying_switch_3",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
@@ -47,6 +42,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -88,15 +84,11 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
lst_switches = [config_entry.get(CONF_HEATER)]
|
||||
if config_entry.get(CONF_HEATER_2):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_2))
|
||||
if config_entry.get(CONF_HEATER_3):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_3))
|
||||
if config_entry.get(CONF_HEATER_4):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_4))
|
||||
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
@@ -136,20 +128,14 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
under0: UnderlyingSwitch = self._underlyings[0]
|
||||
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
|
||||
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_switch_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
@@ -165,6 +151,9 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._attr_extra_state_attributes["function"] = self._proportional_function
|
||||
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
|
||||
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
|
||||
self._attr_extra_state_attributes[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -186,7 +175,8 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
# already done bu update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -198,7 +188,23 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
if not self.is_over_climate and self.mean_cycle_power is not None:
|
||||
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
|
||||
self._total_energy += added_energy
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
@@ -207,7 +213,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_switch_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
def _async_switch_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle heater switch state changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
|
||||
@@ -8,18 +8,14 @@ from homeassistant.helpers.event import (
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import (
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_UNDERLYING_LIST,
|
||||
# This is not really self-regulation but regulation here
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
@@ -29,31 +25,29 @@ from .const import (
|
||||
from .underlyings import UnderlyingValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_ENERGY = logging.getLogger(
|
||||
"custom_components.versatile_thermostat.energy_debug"
|
||||
)
|
||||
|
||||
|
||||
class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_valve_0",
|
||||
"underlying_valve_1",
|
||||
"underlying_valve_2",
|
||||
"underlying_valve_3",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
}
|
||||
)
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -105,15 +99,11 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
lst_valves = [config_entry.get(CONF_VALVE)]
|
||||
if config_entry.get(CONF_VALVE_2):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_2))
|
||||
if config_entry.get(CONF_VALVE_3):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_3))
|
||||
if config_entry.get(CONF_VALVE_4):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_4))
|
||||
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
|
||||
for _, valve in enumerate(lst_valves):
|
||||
self._underlyings.append(
|
||||
@@ -148,7 +138,7 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_valve_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
async def _async_valve_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle unerdlying valve state changes.
|
||||
This method just log the change. It changes nothing to avoid loops.
|
||||
"""
|
||||
@@ -165,18 +155,10 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
"valve_open_percent"
|
||||
] = self.valve_open_percent
|
||||
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
||||
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
@@ -202,6 +184,9 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
self._attr_extra_state_attributes[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -241,10 +226,16 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
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 (
|
||||
dpercent >= -1 * self._auto_regulation_dpercent
|
||||
and dpercent < self._auto_regulation_dpercent
|
||||
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",
|
||||
@@ -266,7 +257,8 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
# already done in update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -278,7 +270,23 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
if not self.is_over_climate and self.mean_cycle_power is not None:
|
||||
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
|
||||
self._total_energy += added_energy
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - get_my_previous_state increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"auto_regulation_dtemp": "Όριο ρύθμισης",
|
||||
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
|
||||
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
|
||||
@@ -64,7 +64,7 @@
|
||||
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
|
||||
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
|
||||
"inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -216,7 +216,7 @@
|
||||
"auto_regulation_dtemp": "Όριο ρύθμισης",
|
||||
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
|
||||
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
|
||||
@@ -237,7 +237,7 @@
|
||||
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
|
||||
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
|
||||
"inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -37,7 +38,8 @@
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
@@ -49,6 +51,8 @@
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -60,57 +64,36 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -259,6 +242,7 @@
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -269,7 +253,8 @@
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
@@ -281,6 +266,8 @@
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -292,57 +279,36 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -508,6 +474,14 @@
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -518,7 +492,8 @@
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Safety",
|
||||
"none": "Manual"
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"power": "Gestion de la puissance",
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"finalize": "Finaliser la création",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
@@ -37,8 +38,9 @@
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"thermostat_type": "Type de thermostat",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
|
||||
"temperature_sensor_entity_id": "Capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
|
||||
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
@@ -49,7 +51,9 @@
|
||||
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
|
||||
},
|
||||
"data_description": {
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure."
|
||||
"temperature_sensor_entity_id": "Id d'entité du capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)",
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
@@ -60,7 +64,8 @@
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante."
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -271,6 +276,7 @@
|
||||
"power": "Gestion de la puissance",
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"finalize": "Finaliser les modifications",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
@@ -281,19 +287,22 @@
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"thermostat_type": "Type de thermostat",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
|
||||
"temperature_sensor_entity_id": "Capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
|
||||
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...).",
|
||||
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
|
||||
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
|
||||
},
|
||||
"data_description": {
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée."
|
||||
"temperature_sensor_entity_id": "Id d'entité du capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)",
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
@@ -304,7 +313,8 @@
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante."
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -331,7 +341,7 @@
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
@@ -526,6 +536,14 @@
|
||||
"comfort": "Confort",
|
||||
"boost": "Renforcé (boost)"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -536,7 +554,8 @@
|
||||
"state": {
|
||||
"power": "Délestage",
|
||||
"security": "Sécurité",
|
||||
"none": "Manuel"
|
||||
"none": "Manuel",
|
||||
"frost": "Hors Gel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
@@ -62,7 +62,7 @@
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Regolazione automatica della temperatura target",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -206,7 +206,7 @@
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
@@ -226,7 +226,7 @@
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -364,7 +364,8 @@
|
||||
"state": {
|
||||
"power": "Ripartizione",
|
||||
"security": "Sicurezza",
|
||||
"none": "Manuale"
|
||||
"none": "Manuale",
|
||||
"frost": "Gelo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,25 @@
|
||||
"thermostat_type": "Len jeden centrálny typ konfigurácie je možný"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
|
||||
"menu_options": {
|
||||
"main": "Hlavné atribúty",
|
||||
"central_boiler": "Centrálny kotol",
|
||||
"type": "Podklady",
|
||||
"tpi": "TPI parametre",
|
||||
"features": "Vlastnosti",
|
||||
"presets": "Predvoľby",
|
||||
"window": "Detekcia okien",
|
||||
"motion": "Detekcia pohybu",
|
||||
"power": "Správa napájania",
|
||||
"presence": "Detekcia prítomnosti",
|
||||
"advanced": "Pokročilé parametre",
|
||||
"finalize": "Všetko hotové",
|
||||
"configuration_not_complete": "Konfigurácia nie je dokončená"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Pridajte nový všestranný termostat",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
@@ -19,22 +38,32 @@
|
||||
"name": "Názov",
|
||||
"thermostat_type": "Termostat typ",
|
||||
"temperature_sensor_entity_id": "ID entity snímača teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
|
||||
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
|
||||
"cycle_min": "Trvanie cyklu (minúty)",
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"step_temperature": "Krok teploty",
|
||||
"device_power": "Napájanie zariadenia",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
|
||||
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Vlastnosti",
|
||||
"description": "Vlastnosti termostatu",
|
||||
"data": {
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
|
||||
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú hlavnú konfiguráciu pre tento VTherm",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -45,6 +74,7 @@
|
||||
"heater_entity2_id": "2. spínač ohrievača",
|
||||
"heater_entity3_id": "3. spínač ohrievača",
|
||||
"heater_entity4_id": "4. spínač ohrievača",
|
||||
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
|
||||
"proportional_function": "Algoritmus",
|
||||
"climate_entity_id": "1. základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
@@ -58,6 +88,7 @@
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
@@ -66,6 +97,7 @@
|
||||
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
|
||||
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
|
||||
"climate_entity_id": "ID základnej klimatickej entity",
|
||||
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
|
||||
@@ -79,6 +111,7 @@
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
@@ -101,24 +134,7 @@
|
||||
"title": "Predvoľby",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavení Frost protection",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
},
|
||||
"data_description": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -130,7 +146,8 @@
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
|
||||
"window_action": "Akcia"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
@@ -138,7 +155,8 @@
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
|
||||
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -181,26 +199,11 @@
|
||||
"title": "Riadenie prítomnosti",
|
||||
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
|
||||
"presence_sensor_entity_id": "Senzora prítomnosti",
|
||||
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické teplotné entity, zrušte výber"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -244,6 +247,25 @@
|
||||
"thermostat_type": "Je možný len jeden centrálny typ konfigurácie"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
|
||||
"menu_options": {
|
||||
"main": "Hlavné atribúty",
|
||||
"central_boiler": "Centrálny kotol",
|
||||
"type": "Podklady",
|
||||
"tpi": "TPI parametre",
|
||||
"features": "Vlastnosti",
|
||||
"presets": "Predvoľby",
|
||||
"window": "Detekcia okien",
|
||||
"motion": "Detekcia pohybu",
|
||||
"power": "Správa napájania",
|
||||
"presence": "Detekcia prítomnosti",
|
||||
"advanced": "Pokročilé parametre",
|
||||
"finalize": "Všetko hotové",
|
||||
"configuration_not_complete": "Konfigurácia nie je dokončená"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Hlavný - {name}",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
@@ -251,22 +273,32 @@
|
||||
"name": "Názov",
|
||||
"thermostat_type": "Termostat typ",
|
||||
"temperature_sensor_entity_id": "ID entity snímača teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
|
||||
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
|
||||
"cycle_min": "Trvanie cyklu (minúty)",
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"step_temperature": "Krok teploty",
|
||||
"device_power": "Výkon zariadenia (kW)",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (vyžaduje centrálnu konfiguráciu). Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode.",
|
||||
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
|
||||
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Vlastnosti - {name}",
|
||||
"description": "Vlastnosti termostatu",
|
||||
"data": {
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
|
||||
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Ak chcete použiť špecifickú konfiguráciu pre tento VTherm, zrušte začiarknutie",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -277,6 +309,7 @@
|
||||
"heater_entity2_id": "2. spínač ohrievača",
|
||||
"heater_entity3_id": "3. spínač ohrievača",
|
||||
"heater_entity4_id": "4. spínač ohrievača",
|
||||
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
|
||||
"proportional_function": "Algoritmus",
|
||||
"climate_entity_id": "Základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
@@ -290,6 +323,7 @@
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
@@ -298,6 +332,7 @@
|
||||
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
|
||||
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
|
||||
"climate_entity_id": "ID základnej klimatickej entity",
|
||||
"climate_entity2_id": "2. základný identifikátor klimatickej entity",
|
||||
@@ -311,6 +346,7 @@
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
@@ -333,24 +369,7 @@
|
||||
"title": "Predvoľby - {name}",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavení Frost protection",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
},
|
||||
"data_description": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -362,7 +381,8 @@
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
|
||||
"window_action": "Akcia"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
@@ -370,7 +390,8 @@
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
|
||||
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -410,29 +431,14 @@
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Riadenie prítomnosti",
|
||||
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
|
||||
"title": "Prítommnosť - {name}",
|
||||
"description": "Atribúty riadenia prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, je niekto prítomný) a poskytuje zodpovedajúce prednastavené nastavenie teploty.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti (pravda je prítomná)",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
|
||||
"presence_sensor_entity_id": "Senzor prítomnosti",
|
||||
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické entity teploty, zrušte začiarknutie"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -458,7 +464,8 @@
|
||||
"unknown": "Neočakávaná chyba",
|
||||
"unknown_entity": "Neznáme ID entity",
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.",
|
||||
"service_configuration_format": "Formát konfigurácie služby je nesprávny"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované"
|
||||
@@ -491,6 +498,22 @@
|
||||
"auto_fan_high": "Vysoký",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Vypnúť",
|
||||
"window_fan_only": "Len ventilátor",
|
||||
"window_frost_temp": "Ochrana pred mrazom",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Ochrana proti mrazu",
|
||||
"eco": "Eco",
|
||||
"comfort": "Komfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -506,6 +529,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"frost_temp": {
|
||||
"name": "Mráz"
|
||||
},
|
||||
"eco_temp": {
|
||||
"name": "Eco"
|
||||
},
|
||||
"comfort_temp": {
|
||||
"name": "Komfort"
|
||||
},
|
||||
"boost_temp": {
|
||||
"name": "Boost"
|
||||
},
|
||||
"frost_ac_temp": {
|
||||
"name": "Mráz ac"
|
||||
},
|
||||
"eco_ac_temp": {
|
||||
"name": "Eco ac"
|
||||
},
|
||||
"comfort_ac_temp": {
|
||||
"name": "Komfort ac"
|
||||
},
|
||||
"boost_ac_temp": {
|
||||
"name": "Boost ac"
|
||||
},
|
||||
"frost_away_temp": {
|
||||
"name": "Mráz mimo"
|
||||
},
|
||||
"eco_away_temp": {
|
||||
"name": "Eko mimo"
|
||||
},
|
||||
"comfort_away_temp": {
|
||||
"name": "Komfort mimo"
|
||||
},
|
||||
"boost_away_temp": {
|
||||
"name": "Boost mimo"
|
||||
},
|
||||
"eco_ac_away_temp": {
|
||||
"name": "Eco ac mimo"
|
||||
},
|
||||
"comfort_ac_away_temp": {
|
||||
"name": "Komfort ac mimo"
|
||||
},
|
||||
"boost_ac_away_temp": {
|
||||
"name": "Boost ac mimo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import State
|
||||
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
@@ -30,6 +30,7 @@ from homeassistant.components.number import SERVICE_SET_VALUE
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import UnknownEntity, overrides
|
||||
from .keep_alive import IntervalCaller
|
||||
@@ -188,7 +189,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
thermostat: Any,
|
||||
switch_entity_id: str,
|
||||
initial_delay_sec: int,
|
||||
keep_alive_sec: int,
|
||||
keep_alive_sec: float,
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
@@ -217,6 +218,11 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Tells if the switch command should be inversed"""
|
||||
return self._thermostat.is_inversed
|
||||
|
||||
@property
|
||||
def keep_alive_sec(self) -> float:
|
||||
"""Return the switch keep-alive interval in seconds."""
|
||||
return self._keep_alive.interval_sec
|
||||
|
||||
@overrides
|
||||
def startup(self):
|
||||
super().startup()
|
||||
@@ -247,7 +253,28 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
|
||||
async def _keep_alive_callback(self):
|
||||
"""Keep alive: Turn on if already turned on, turn off if already turned off."""
|
||||
await (self.turn_on() if self.is_device_active else self.turn_off())
|
||||
timer = self._keep_alive.backoff_timer
|
||||
state: State | None = self._hass.states.get(self._entity_id)
|
||||
# Normal, expected state.state values are "on" and "off". An absent
|
||||
# underlying MQTT switch was observed to produce either state == None
|
||||
# or state.state == STATE_UNAVAILABLE ("unavailable").
|
||||
if state is None or state.state == STATE_UNAVAILABLE:
|
||||
if timer.is_ready():
|
||||
_LOGGER.warning(
|
||||
"Entity %s is not available (state: %s). Will keep trying "
|
||||
"keep alive calls, but won't log this condition every time.",
|
||||
self._entity_id,
|
||||
state.state if state else "None",
|
||||
)
|
||||
else:
|
||||
if timer.in_progress:
|
||||
timer.reset()
|
||||
_LOGGER.warning(
|
||||
"Entity %s has recovered (state: %s).",
|
||||
self._entity_id,
|
||||
state.state,
|
||||
)
|
||||
await (self.turn_on() if self.is_device_active else self.turn_off())
|
||||
|
||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||
async def turn_off(self):
|
||||
@@ -461,6 +488,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
entity_id=climate_entity_id,
|
||||
)
|
||||
self._underlying_climate = None
|
||||
self._last_sent_temperature = None
|
||||
|
||||
def find_underlying_climate(self) -> ClimateEntity:
|
||||
"""Find the underlying climate entity"""
|
||||
@@ -481,8 +509,8 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
self._underlying_climate,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
||||
_LOGGER.info(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational. Will try later.",
|
||||
self,
|
||||
self.entity_id,
|
||||
)
|
||||
@@ -522,14 +550,11 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self.is_initialized:
|
||||
return (
|
||||
self._underlying_climate.hvac_mode != HVACMode.OFF
|
||||
and self._underlying_climate.hvac_action
|
||||
not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
)
|
||||
return self.hvac_mode != HVACMode.OFF and self.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
None,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -585,12 +610,24 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
if not self.is_initialized:
|
||||
return
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": self.cap_sent_value(temperature),
|
||||
"target_temp_high": max_temp,
|
||||
"target_temp_low": min_temp,
|
||||
}
|
||||
# Issue 508 we have to take care of service set_temperature or set_range
|
||||
target_temp = self.cap_sent_value(temperature)
|
||||
if (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
in self._underlying_climate.supported_features
|
||||
):
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"target_temp_high": target_temp,
|
||||
"target_temp_low": target_temp,
|
||||
# issue 518 - we should send also the target temperature, even in TARGET RANGE
|
||||
"temperature": target_temp,
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": target_temp,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -598,12 +635,48 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
data,
|
||||
)
|
||||
|
||||
self._last_sent_temperature = target_temp
|
||||
|
||||
@property
|
||||
def last_sent_temperature(self) -> float | None:
|
||||
"""Get the last send temperature. None if no temperature have been sent yet"""
|
||||
return self._last_sent_temperature
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Get the hvac action of the underlying"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.hvac_action
|
||||
|
||||
hvac_action = self._underlying_climate.hvac_action
|
||||
if hvac_action is None:
|
||||
target = (
|
||||
self.underlying_target_temperature
|
||||
or self._thermostat.target_temperature
|
||||
)
|
||||
current = (
|
||||
self.underlying_current_temperature
|
||||
or self._thermostat.current_temperature
|
||||
)
|
||||
hvac_mode = self.hvac_mode
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - hvac_action simulation target=%s, current=%s, hvac_mode=%s",
|
||||
self,
|
||||
target,
|
||||
current,
|
||||
hvac_mode,
|
||||
)
|
||||
hvac_action = HVACAction.IDLE
|
||||
if target is not None and current is not None:
|
||||
dtemp = target - current
|
||||
|
||||
if hvac_mode == HVACMode.COOL and dtemp < 0:
|
||||
hvac_action = HVACAction.COOLING
|
||||
elif hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL] and dtemp > 0:
|
||||
hvac_action = HVACAction.HEATING
|
||||
|
||||
return hvac_action
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
@@ -658,7 +731,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def temperature_unit(self) -> str:
|
||||
"""Get the temperature_unit"""
|
||||
if not self.is_initialized:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self._hass.config.units.temperature_unit
|
||||
return self._underlying_climate.temperature_unit
|
||||
|
||||
@property
|
||||
@@ -683,11 +756,19 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return self._underlying_climate.target_temperature_low
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Get the is_aux_heat"""
|
||||
def underlying_target_temperature(self) -> float:
|
||||
"""Get the target_temperature"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
return self._underlying_climate.is_aux_heat
|
||||
return None
|
||||
|
||||
if not hasattr(self._underlying_climate, "target_temperature"):
|
||||
return None
|
||||
else:
|
||||
return self._underlying_climate.target_temperature
|
||||
|
||||
# return self._hass.states.get(self._entity_id).attributes.get(
|
||||
# "target_temperature"
|
||||
# )
|
||||
|
||||
@property
|
||||
def underlying_current_temperature(self) -> float | None:
|
||||
@@ -698,8 +779,17 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
|
||||
if not hasattr(self._underlying_climate, "current_temperature"):
|
||||
return None
|
||||
else:
|
||||
return self._underlying_climate.current_temperature
|
||||
|
||||
return self._underlying_climate.current_temperature
|
||||
# return self._hass.states.get(self._entity_id).attributes.get("current_temperature")
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Get the is_aux_heat"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
return self._underlying_climate.is_aux_heat
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
@@ -726,8 +816,12 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
self._underlying_climate.min_temp is not None
|
||||
and self._underlying_climate is not None
|
||||
):
|
||||
min_val = self._underlying_climate.min_temp
|
||||
max_val = self._underlying_climate.max_temp
|
||||
min_val = TemperatureConverter.convert(
|
||||
self._underlying_climate.min_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
|
||||
)
|
||||
max_val = TemperatureConverter.convert(
|
||||
self._underlying_climate.max_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
|
||||
)
|
||||
|
||||
new_value = max(min_val, min(value, max_val))
|
||||
else:
|
||||
@@ -775,15 +869,19 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
"""Send the percent open to the underlying valve"""
|
||||
# This may fails if called after shutdown
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
|
||||
data = {"value": self._percent_open}
|
||||
target = {ATTR_ENTITY_ID: self._entity_id}
|
||||
domain = self._entity_id.split(".")[0]
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
SERVICE_SET_VALUE,
|
||||
data,
|
||||
domain=domain,
|
||||
service=SERVICE_SET_VALUE,
|
||||
service_data=data,
|
||||
target=target,
|
||||
)
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
# This could happens in unit test if input_number domain is not yet loaded
|
||||
# raise err
|
||||
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
@@ -835,8 +933,10 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
if force:
|
||||
self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
await self.send_percent_open()
|
||||
# self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
# await self.send_percent_open()
|
||||
# avoid to send 2 times the same value at startup
|
||||
self.set_valve_open_percent()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
@@ -852,7 +952,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
min_val = valve_state.attributes["min"]
|
||||
max_val = valve_state.attributes["max"]
|
||||
|
||||
new_value = round(max(min_val, min(value, max_val)))
|
||||
new_value = round(max(min_val, min(value / 100 * max_val, max_val)))
|
||||
else:
|
||||
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||
new_value = value
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
VTHERM_API_NAME = "vtherm_api"
|
||||
@@ -57,8 +58,10 @@ class VersatileThermostatAPI(dict):
|
||||
self._threshold_number_entity = None
|
||||
self._nb_active_number_entity = None
|
||||
self._central_configuration = None
|
||||
self._central_mode_select = None
|
||||
# A dict that will store all Number entities which holds the temperature
|
||||
self._number_temperatures = dict()
|
||||
self._max_on_percent = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
@@ -106,6 +109,12 @@ class VersatileThermostatAPI(dict):
|
||||
if self._safety_mode:
|
||||
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
|
||||
|
||||
self._max_on_percent = config.get(CONF_MAX_ON_PERCENT)
|
||||
if self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"We have found max_on_percent setting %s", self._max_on_percent
|
||||
)
|
||||
|
||||
def register_central_boiler(self, central_boiler_entity):
|
||||
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
|
||||
class to register itself at creation"""
|
||||
@@ -149,10 +158,11 @@ class VersatileThermostatAPI(dict):
|
||||
return entity.state
|
||||
return None
|
||||
|
||||
async def init_vtherm_links(self, only_use_central=False):
|
||||
"""INitialize all VTherms entities links
|
||||
async def init_vtherm_links(self, entry_id=None):
|
||||
"""Initialize all VTherms entities links
|
||||
This method is called when HA is fully started (and all entities should be initialized)
|
||||
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
|
||||
If entry_id is set, only the VTherm of this entry will be reloaded
|
||||
"""
|
||||
await self.reload_central_boiler_binary_listener()
|
||||
await self.reload_central_boiler_entities_list()
|
||||
@@ -162,12 +172,35 @@ class VersatileThermostatAPI(dict):
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
if hasattr(entity, "init_presets"):
|
||||
if (
|
||||
only_use_central is False
|
||||
or entity.use_central_config_temperature
|
||||
):
|
||||
await entity.init_presets(self.find_central_configuration())
|
||||
# if hasattr(entity, "init_presets"):
|
||||
# if (
|
||||
# only_use_central is False
|
||||
# or entity.use_central_config_temperature
|
||||
# ):
|
||||
# await entity.init_presets(self.find_central_configuration())
|
||||
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance due to circular dependency of BaseThermostat
|
||||
if (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
):
|
||||
if entry_id is None or entry_id == entity.unique_id:
|
||||
await entity.async_startup(self.find_central_configuration())
|
||||
|
||||
async def init_vtherm_preset_with_central(self):
|
||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||
# Initialization of all preset for all VTherm
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data.get(
|
||||
CLIMATE_DOMAIN, None
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
if (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
and entity.use_central_config_temperature
|
||||
):
|
||||
await entity.init_presets(self.find_central_configuration())
|
||||
|
||||
async def reload_central_boiler_binary_listener(self):
|
||||
"""Reloads the BinarySensor entity which listen to the number of
|
||||
@@ -180,6 +213,27 @@ class VersatileThermostatAPI(dict):
|
||||
if self._nb_active_number_entity is not None:
|
||||
await self._nb_active_number_entity.listen_vtherms_entities()
|
||||
|
||||
def register_central_mode_select(self, central_mode_select):
|
||||
"""Register the select entity which holds the central_mode"""
|
||||
self._central_mode_select = central_mode_select
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
if self._central_mode_select is None:
|
||||
return
|
||||
|
||||
# Update all VTherm states
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.device_info and entity.device_info.get("model", None) == DOMAIN:
|
||||
_LOGGER.debug(
|
||||
"Changing the central_mode. We have find %s to update",
|
||||
entity.name,
|
||||
)
|
||||
await entity.check_central_mode(
|
||||
self._central_mode_select.state, old_central_mode
|
||||
)
|
||||
|
||||
@property
|
||||
def self_regulation_expert(self):
|
||||
"""Get the self regulation params"""
|
||||
@@ -229,6 +283,14 @@ class VersatileThermostatAPI(dict):
|
||||
return None
|
||||
return int(self._threshold_number_entity.native_value)
|
||||
|
||||
@property
|
||||
def central_mode(self) -> str | None:
|
||||
"""Get the current central mode or None"""
|
||||
if self._central_mode_select:
|
||||
return self._central_mode_select.state
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"hide_default_branch": false,
|
||||
"homeassistant": "2024.3.1"
|
||||
"homeassistant": "2024.10.4"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
homeassistant==2024.3.1
|
||||
homeassistant==2024.10.4
|
||||
|
||||
156
tests/commons.py
156
tests/commons.py
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
|
||||
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
@@ -435,6 +435,86 @@ class MagicMockClimate(MagicMock):
|
||||
return 19
|
||||
|
||||
|
||||
class MagicMockClimateWithTemperatureRange(MagicMock):
|
||||
"""A Magic Mock class for a underlying climate entity"""
|
||||
|
||||
@property
|
||||
def temperature_unit(self): # pylint: disable=missing-function-docstring
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
def hvac_mode(self): # pylint: disable=missing-function-docstring
|
||||
return HVACMode.HEAT
|
||||
|
||||
@property
|
||||
def hvac_action(self): # pylint: disable=missing-function-docstring
|
||||
return HVACAction.IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self): # pylint: disable=missing-function-docstring
|
||||
return 15
|
||||
|
||||
@property
|
||||
def current_temperature(self): # pylint: disable=missing-function-docstring
|
||||
return 14
|
||||
|
||||
@property
|
||||
def target_temperature_step( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> float | None:
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def target_temperature_high( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> float | None:
|
||||
return 35
|
||||
|
||||
@property
|
||||
def target_temperature_low( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> float | None:
|
||||
return 7
|
||||
|
||||
@property
|
||||
def hvac_modes( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> list[str] | None:
|
||||
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
|
||||
|
||||
@property
|
||||
def fan_modes( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> list[str] | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_modes( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> list[str] | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None: # pylint: disable=missing-function-docstring
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str | None: # pylint: disable=missing-function-docstring
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||
return ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
|
||||
@property
|
||||
def min_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 10
|
||||
|
||||
@property
|
||||
def max_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 31
|
||||
|
||||
|
||||
class MockSwitch(SwitchEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
@@ -472,7 +552,14 @@ class MockNumber(NumberEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos={}
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name,
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
entry_infos={},
|
||||
):
|
||||
"""Init the switch"""
|
||||
super().__init__()
|
||||
@@ -482,7 +569,9 @@ class MockNumber(NumberEntity):
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._name = name
|
||||
self._attr_native_value = 0
|
||||
self._attr_native_min_value = 0
|
||||
self._attr_native_min_value = min
|
||||
self._attr_native_max_value = max
|
||||
self._attr_step = step
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -580,6 +669,31 @@ async def send_temperature_change_event(
|
||||
return dearm_window_auto
|
||||
|
||||
|
||||
async def send_last_seen_temperature_change_event(
|
||||
entity: BaseThermostat, date, sleep=True
|
||||
):
|
||||
"""Sending a new last seen event simulating a change on last seen temperature sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_last_seen_temperature_change_event, date=%s on %s",
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
last_seen_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=date,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_last_seen_temperature_changed(last_seen_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_ext_temperature_change_event(
|
||||
entity: BaseThermostat, new_temp, date, sleep=True
|
||||
):
|
||||
@@ -817,6 +931,7 @@ async def send_climate_change_event_with_temperature(
|
||||
date,
|
||||
temperature,
|
||||
sleep=True,
|
||||
underlying_entity_id=None,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
@@ -829,18 +944,21 @@ async def send_climate_change_event_with_temperature(
|
||||
temperature,
|
||||
entity,
|
||||
)
|
||||
if not underlying_entity_id:
|
||||
underlying_entity_id = entity.entity_id
|
||||
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=underlying_entity_id,
|
||||
state=new_hvac_mode,
|
||||
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=underlying_entity_id,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
@@ -882,3 +1000,31 @@ async def set_climate_preset_temp(
|
||||
)
|
||||
if temp_entity:
|
||||
await temp_entity.async_set_native_value(temp)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
|
||||
number_entity_id,
|
||||
)
|
||||
|
||||
|
||||
async def set_all_climate_preset_temp(
|
||||
hass, vtherm: BaseThermostat, temps: dict, number_entity_base_name: str
|
||||
):
|
||||
"""Initialize all temp of preset for a VTherm entity"""
|
||||
# We initialize
|
||||
for preset_name, value in temps.items():
|
||||
|
||||
await set_climate_preset_temp(vtherm, preset_name, value)
|
||||
|
||||
# Search the number entity to control it is correctly set
|
||||
number_entity_name = (
|
||||
f"number.{number_entity_base_name}_preset_{preset_name}{PRESET_TEMP_SUFFIX}"
|
||||
)
|
||||
temp_entity: NumberEntity = search_entity(
|
||||
hass,
|
||||
number_entity_name,
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
assert temp_entity
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
@@ -74,7 +74,7 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
@@ -82,17 +82,14 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_air_conditioner",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_air_conditioner"],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_4switch0",
|
||||
CONF_HEATER_2: "switch.mock_4switch1",
|
||||
CONF_HEATER_3: "switch.mock_4switch2",
|
||||
CONF_HEATER_4: "switch.mock_4switch3",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_4switch0", "switch.mock_4switch1","switch.mock_4switch2","switch.mock_4switch3"],
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
@@ -105,7 +102,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
@@ -115,7 +112,7 @@ MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.1,
|
||||
@@ -125,13 +122,13 @@ MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
|
||||
@@ -53,18 +53,6 @@ async def test_over_climate_regulation(
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -163,18 +151,6 @@ async def test_over_climate_regulation_ac_mode(
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -544,3 +520,112 @@ async def test_over_climate_regulation_use_device_temp(
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_dtemp_null(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat with no Dtemp limitation"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation
|
||||
data=PARTIAL_CLIMATE_AC_CONFIG | {CONF_AUTO_REGULATION_DTEMP: 0, CONF_STEP_TEMPERATURE: 0.1},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# Creates the regulated VTherm over climate
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
|
||||
# Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.OFF
|
||||
|
||||
# change temperature so that the heating will start
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=17)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=20)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under cooling
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be lower
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert (
|
||||
entity.regulated_target_temp == 20 + 2.4
|
||||
) # In medium we could go up to +3 degre
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 20 + 0.9
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=13)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 20 + 0.5
|
||||
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
# Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 19.6, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater. This does not work if dtemp is not null
|
||||
assert entity.regulated_target_temp > old_regulated_temp
|
||||
|
||||
1465
tests/test_auto_start_stop.py
Normal file
1465
tests/test_auto_start_stop.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
@@ -8,92 +8,27 @@ from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.config_flow import (
|
||||
VersatileThermostatBaseConfigFlow,
|
||||
)
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_56(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that in over_climate mode there is no error when underlying climate is not available"""
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
# cause the underlying climate was not found
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.underlying_entity(0)._underlying_climate is None
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
ret = await entity.async_control_heating()
|
||||
# an exception should be send
|
||||
assert ret is False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying, # dont find the underlying climate
|
||||
):
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
await entity.async_control_heating()
|
||||
except UnknownEntity:
|
||||
assert False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_63(
|
||||
@@ -193,391 +128,6 @@ async def test_bug_64(
|
||||
assert entity
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to open/close the window rapidly without side effect"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Open the window and let the thermostat shut down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window but too shortly
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should not have change
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Reopen immediatly with sufficient time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# still no change
|
||||
assert entity.window_state == STATE_ON
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
# Close the window but with sufficient time this time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should be Off this time and old state should have been restored
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_82(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
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_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockUnavailableClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}
|
||||
)
|
||||
|
||||
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:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
# assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# assert entity.hvac_mode is None
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force safety mode
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (
|
||||
entity._last_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_101(
|
||||
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")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity = find_my_entity("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
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# 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
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
now,
|
||||
12.75,
|
||||
)
|
||||
# Should NOT have been switched to Manual preset
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
12.75,
|
||||
)
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_272(
|
||||
@@ -657,8 +207,8 @@ async def test_bug_272(
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 17.5,
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
# "target_temp_high": 30,
|
||||
# "target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -687,8 +237,8 @@ async def test_bug_272(
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 15, # the minimum acceptable
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
# "target_temp_high": 30,
|
||||
# "target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -714,8 +264,8 @@ async def test_bug_272(
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 19, # the maximum acceptable
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
# "target_temp_high": 30,
|
||||
# "target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -849,78 +399,217 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
|
||||
async def test_bug_339(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the counter of active Vtherm in central boiler is
|
||||
correctly updated with underlying is in auto and device is active
|
||||
"""
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_500_1(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
config = {
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
|
||||
}
|
||||
|
||||
climate1 = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="climate1",
|
||||
name="theClimate1",
|
||||
hvac_mode=HVACMode.AUTO,
|
||||
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
|
||||
hvac_action=HVACAction.HEATING,
|
||||
)
|
||||
flow = VersatileThermostatBaseConfigFlow(config)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_POWER_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_500_2(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
|
||||
config = {
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
}
|
||||
|
||||
flow = VersatileThermostatBaseConfigFlow(config)
|
||||
|
||||
assert flow._infos[CONF_USE_WINDOW_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_POWER_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
|
||||
config = {
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_POWER_SENSOR: "sensor.thePowerSensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.theMaxPowerSensor",
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
|
||||
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
|
||||
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
|
||||
}
|
||||
|
||||
flow = VersatileThermostatBaseConfigFlow(config)
|
||||
|
||||
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_POWER_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_465(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test store and restore hvac_mode on toggle hvac state"""
|
||||
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# The temperatures to set
|
||||
temps = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"eco_ac": 27.0,
|
||||
"comfort_ac": 25.0,
|
||||
"boost_ac": 23.0,
|
||||
"frost_away": 7.1,
|
||||
"eco_away": 17.1,
|
||||
"comfort_away": 19.1,
|
||||
"boost_away": 21.1,
|
||||
"eco_ac_away": 27.1,
|
||||
"comfort_ac_away": 25.1,
|
||||
"boost_ac_away": 23.1,
|
||||
}
|
||||
|
||||
# 0. initialisation
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
unique_id="overClimateUniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
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: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_DELAY: 1,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
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_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mock_climate",
|
||||
name="mock_climate",
|
||||
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.FAN_ONLY],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
||||
hass, config_entry, "climate.overclimate"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate
|
||||
assert entity.underlying_entities[0].entity_id == "climate.climate1"
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert vtherm is not None
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.AUTO)
|
||||
# Simulate a state change in underelying
|
||||
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
||||
|
||||
# The VTherm should be active
|
||||
assert entity.underlying_entity(0).is_device_active is True
|
||||
assert entity.is_device_active is True
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
now: datetime = datetime.now(tz=get_tz(hass))
|
||||
|
||||
entity.remove_thermostat()
|
||||
# 1. Set mode to Heat and preset to Comfort
|
||||
await send_presence_change_event(vtherm, True, False, datetime.now())
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await vtherm.async_set_preset_mode(PRESET_BOOST)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 21.0
|
||||
|
||||
# 2. Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 3. (re)Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 4. Toggle from COOL
|
||||
await vtherm.async_set_hvac_mode(HVACMode.COOL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 23.0
|
||||
|
||||
# 5. Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 6. (re)Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
###
|
||||
# Same test with an open window and initial state is COOL
|
||||
#
|
||||
# 7. open the window
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
try_window_condition = await send_window_change_event(
|
||||
vtherm, True, False, now, False
|
||||
)
|
||||
await try_window_condition(None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.window_state is STATE_ON
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 8. call toggle -> we should stay in OFF (command is ignored)
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 9. Close the window (we should come back to Cool this time)
|
||||
now = now + timedelta(minutes=2)
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
try_window_condition = await send_window_change_event(
|
||||
vtherm, False, True, now, False
|
||||
)
|
||||
await try_window_condition(None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.window_state is STATE_OFF
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
# 9. call toggle -> we should come back in OFF
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
@@ -69,7 +69,7 @@ async def test_add_a_central_config_with_boiler(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple(
|
||||
@@ -731,7 +731,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
):
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
entity: ThermostatOverClimate = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -842,3 +842,80 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_bug_339(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the counter of active Vtherm in central boiler is
|
||||
correctly updated with underlying is in auto and device is active
|
||||
"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
climate1 = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="climate1",
|
||||
name="theClimate1",
|
||||
hvac_mode=HVACMode.AUTO,
|
||||
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
|
||||
hvac_action=HVACAction.HEATING,
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
):
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate
|
||||
assert entity.underlying_entities[0].entity_id == "climate.climate1"
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.AUTO)
|
||||
# Simulate a state change in underelying
|
||||
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
|
||||
|
||||
# The VTherm should be active
|
||||
assert entity.underlying_entity(0).is_device_active is True
|
||||
assert entity.is_device_active is True
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
@@ -94,8 +94,8 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
assert api.nb_active_device_for_boiler_entity is None
|
||||
assert api.nb_active_device_for_boiler is None
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is None
|
||||
assert api.nb_active_device_for_boiler_threshold is None
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -514,4 +514,4 @@ async def test_migration_of_central_config(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
|
||||
|
||||
@@ -630,6 +630,7 @@ async def test_climate_ac_only_change_central_mode_true(
|
||||
},
|
||||
)
|
||||
|
||||
# 1. set hvac_mode to COOL and preet ECO
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
@@ -982,7 +983,8 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
|
||||
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
|
||||
|
||||
assert entity.last_central_mode is CENTRAL_MODE_COOL_ONLY
|
||||
await entity.async_set_hvac_mode(HVACMode.OFF)
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.hvac_off_reason == HVAC_OFF_REASON_MANUAL
|
||||
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||
assert entity._saved_hvac_mode == HVACMode.HEAT
|
||||
assert entity._saved_preset_mode == PRESET_ACTIVITY
|
||||
@@ -1000,12 +1002,14 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
|
||||
|
||||
await try_function(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
|
||||
)
|
||||
# The VTherm is already off -> window detection is ignored
|
||||
assert mock_send_event.call_count == 0
|
||||
# mock_send_event.assert_has_calls(
|
||||
# [call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
|
||||
# )
|
||||
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
assert entity.hvac_off_reason == HVAC_OFF_REASON_MANUAL
|
||||
assert entity.preset_mode == PRESET_ACTIVITY
|
||||
assert entity._saved_hvac_mode == HVACMode.HEAT
|
||||
assert entity._saved_preset_mode == PRESET_ACTIVITY
|
||||
@@ -1021,6 +1025,8 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
|
||||
assert entity.last_central_mode is CENTRAL_MODE_AUTO
|
||||
# No change
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
# We have to a reason of WINDOW_DETECTION
|
||||
assert entity.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
|
||||
assert entity.preset_mode == PRESET_ACTIVITY
|
||||
assert entity._saved_hvac_mode == HVACMode.HEAT
|
||||
assert entity._saved_preset_mode == PRESET_ACTIVITY
|
||||
@@ -1046,6 +1052,7 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
|
||||
|
||||
# We should stay off because central is STOPPED
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.hvac_off_reason is None
|
||||
assert entity.preset_mode == PRESET_ACTIVITY
|
||||
assert entity._saved_hvac_mode == HVACMode.HEAT
|
||||
assert entity._saved_preset_mode == PRESET_ACTIVITY
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
136
tests/test_last_seen.py
Normal file
136
tests/test_last_seen.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the Security featrure """
|
||||
from unittest.mock import patch, call
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the last_ssen feature
|
||||
1. creates a thermostat and check that security is off
|
||||
2. activate security feature when date is expired
|
||||
3. change the last seen sensor
|
||||
4. check that security is off
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
"name": "TheOverSwitchMockName",
|
||||
"thermostat_type": "thermostat_over_switch",
|
||||
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
|
||||
"last_seen_temperature_sensor_entity_id": "sensor.mock_last_seen_temp_sensor",
|
||||
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
|
||||
"cycle_min": 5,
|
||||
"temp_min": 15,
|
||||
"temp_max": 30,
|
||||
"frost_temp": 7,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"use_window_feature": False,
|
||||
"use_motion_feature": False,
|
||||
"use_power_feature": False,
|
||||
"use_presence_feature": False,
|
||||
"heater_entity_id": "switch.mock_switch",
|
||||
"proportional_function": "tpi",
|
||||
"tpi_coef_int": 0.3,
|
||||
"tpi_coef_ext": 0.01,
|
||||
"minimal_activation_delay": 30,
|
||||
"security_delay_min": 5, # 5 minutes
|
||||
"security_min_on_percent": 0.2,
|
||||
"security_default_on_percent": 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
# 1. creates a thermostat and check that security is off
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# set a preset
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# Turn On the thermostat
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
assert entity.security_state is True
|
||||
assert entity.preset_mode == PRESET_SECURITY
|
||||
|
||||
assert mock_send_event.call_count == 3
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
|
||||
call.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
},
|
||||
),
|
||||
call.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
# 3. change the last seen sensor
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_last_seen_temperature_change_event(entity, event_timestamp)
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity._last_temperature_measure == event_timestamp
|
||||
@@ -83,7 +83,7 @@ async def test_movement_management_time_not_enough(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# starts detecting motion with time not enough
|
||||
with patch(
|
||||
@@ -97,7 +97,12 @@ async def test_movement_management_time_not_enough(
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition:
|
||||
) as mock_condition, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
@@ -109,8 +114,8 @@ async def test_movement_management_time_not_enough(
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
# state is not changed if time is not enough
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state is STATE_OFF
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is not confirmed
|
||||
@@ -141,8 +146,8 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
# stop detecting motion with off delay too low
|
||||
with patch(
|
||||
@@ -156,19 +161,24 @@ async def test_movement_management_time_not_enough(
|
||||
return_value=True,
|
||||
) as mock_device_active, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition:
|
||||
) as mock_condition, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
# Will return False -> we will stay to movement On
|
||||
# Will return False -> we will stay to movement On
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must heat now
|
||||
@@ -192,15 +202,15 @@ async def test_movement_management_time_not_enough(
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
# Will return True -> we will switch to movement Off
|
||||
# Will return True -> we will switch to movement Off
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == STATE_OFF
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must stop heating now
|
||||
@@ -214,7 +224,7 @@ async def test_movement_management_time_not_enough(
|
||||
async def test_movement_management_time_enough_and_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
"""Test the Motion management when time is not enough"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -280,7 +290,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -302,8 +312,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -331,8 +341,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -412,7 +422,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -434,8 +444,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost away mode
|
||||
assert entity.target_temperature == 19.1
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -463,8 +473,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18.1
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
@@ -479,7 +489,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
async def test_movement_management_with_stop_during_condition(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when the movement sensor switch to off and then to on during the test condition"""
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -546,7 +556,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -558,9 +568,13 @@ async def test_movement_management_with_stop_during_condition(
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch("homeassistant.helpers.condition.state", return_value=True): # Not needed for this test
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
): # Not needed for this test
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
try_condition1 = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
try_condition1 = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
assert try_condition1 is not None
|
||||
|
||||
@@ -569,33 +583,155 @@ async def test_movement_management_with_stop_during_condition(
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# Send a stop detection
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
assert try_condition is None # The timer should not have been stopped
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
assert try_condition is None # The timer should not have been stopped
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# Resend a start detection
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
assert try_condition is None # The timer should not have been restarted (we keep the first one)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
assert (
|
||||
try_condition is None
|
||||
) # The timer should not have been restarted (we keep the first one)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# still no motion detected
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
await try_condition1(None)
|
||||
# We should have switch this time
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state is "on" # switch to movement on
|
||||
assert entity.presence_state is "off" # Non change
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state == "on" # switch to movement on
|
||||
assert entity.presence_state == "off" # Non change
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_with_stop_during_condition_last_state_on(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17,
|
||||
"comfort_away_temp": 18,
|
||||
"boost_away_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 0. start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# 1. starts detecting motion but the sensor is off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
try_condition1 = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
assert try_condition1 is not None
|
||||
|
||||
await try_condition1(None)
|
||||
|
||||
# because no motion is detected yet -> condition.state is False and sensor is not active
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is STATE_OFF
|
||||
|
||||
# 2. starts detecting motion but the sensor is on
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
try_condition1 = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
assert try_condition1 is not None
|
||||
|
||||
await try_condition1(None)
|
||||
|
||||
# because no motion is detected yet -> condition.state is False and sensor is not active
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is STATE_ON
|
||||
|
||||
@@ -596,6 +596,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
|
||||
1201
tests/test_overclimate.py
Normal file
1201
tests/test_overclimate.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -433,6 +433,7 @@ async def test_power_management_energy_over_climate(
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
# We have the start event and not the end event
|
||||
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
|
||||
@@ -448,6 +449,7 @@ async def test_power_management_energy_over_climate(
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
# We have the end event -> we should have some power and on_percent
|
||||
assert entity._underlying_climate_start_hvac_action_date is None
|
||||
|
||||
@@ -283,6 +283,7 @@ async def test_sensors_over_climate(
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
|
||||
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
||||
@@ -293,6 +294,7 @@ async def test_sensors_over_climate(
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
|
||||
# 60 minutes heating with 1.5 kW heating -> 1.5 kWh
|
||||
|
||||
@@ -254,6 +254,9 @@ async def test_over_switch_deactivate_preset(
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_SECURITY_DELAY_MIN: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.6,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import ANY, _Call, call, patch
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
|
||||
from custom_components.versatile_thermostat.keep_alive import BackoffTimer
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
@@ -52,6 +53,7 @@ class CommonMocks:
|
||||
hass: HomeAssistant
|
||||
thermostat: ThermostatOverSwitch
|
||||
mock_is_state: MagicMock
|
||||
mock_get_state: MagicMock
|
||||
mock_service_call: MagicMock
|
||||
mock_async_track_time_interval: MagicMock
|
||||
mock_send_event: MagicMock
|
||||
@@ -73,15 +75,18 @@ async def common_mocks(
|
||||
thermostat = cast(ThermostatOverSwitch, await create_thermostat(
|
||||
hass, config_entry, "climate.theoverswitchmockname"
|
||||
))
|
||||
yield CommonMocks(
|
||||
config_entry=config_entry,
|
||||
hass=hass,
|
||||
thermostat=thermostat,
|
||||
mock_is_state=mock_is_state,
|
||||
mock_service_call=mock_service_call,
|
||||
mock_async_track_time_interval=mock_async_track_time_interval,
|
||||
mock_send_event=mock_send_event,
|
||||
)
|
||||
with patch("homeassistant.core.StateMachine.get") as mock_get_state:
|
||||
mock_get_state.return_value.state = "off"
|
||||
yield CommonMocks(
|
||||
config_entry=config_entry,
|
||||
hass=hass,
|
||||
thermostat=thermostat,
|
||||
mock_is_state=mock_is_state,
|
||||
mock_get_state=mock_get_state,
|
||||
mock_service_call=mock_service_call,
|
||||
mock_async_track_time_interval=mock_async_track_time_interval,
|
||||
mock_send_event=mock_send_event,
|
||||
)
|
||||
# Clean the entity
|
||||
thermostat.remove_thermostat()
|
||||
|
||||
@@ -256,3 +261,123 @@ class TestKeepAlive:
|
||||
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestBackoffTimer:
|
||||
"""Test the keep_alive.BackoffTimer helper class."""
|
||||
|
||||
def test_exponential_period_increase(self):
|
||||
"""Test that consecutive calls to is_ready() produce increasing wait periods."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 129
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 130
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 188
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 189
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 190
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 309
|
||||
assert not timer.is_ready()
|
||||
|
||||
def test_the_upper_limit_option(self):
|
||||
"""Test the timer.in_progress property and the effect of timer.reset()."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=50,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 129
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 130
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 178
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 179
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 180
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 229
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 230
|
||||
assert timer.is_ready()
|
||||
|
||||
def test_the_lower_limit_option(self):
|
||||
"""Test the timer.in_progress property and the effect of timer.reset()."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=0.5,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=50,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 129
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 130
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 158
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 159
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 160
|
||||
assert timer.is_ready()
|
||||
|
||||
def test_initial_is_ready_result(self):
|
||||
"""Test that the first call to is_ready() produces the initially_ready option value."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
for initial in [True, False]:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=initial,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready() == initial
|
||||
assert not timer.is_ready()
|
||||
|
||||
def test_in_progress_and_reset(self):
|
||||
"""Test the timer.in_progress property and the effect of timer.reset()."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert not timer.in_progress
|
||||
assert timer.is_ready()
|
||||
assert timer.in_progress
|
||||
assert not timer.is_ready()
|
||||
timer.reset()
|
||||
assert not timer.in_progress
|
||||
assert timer.is_ready()
|
||||
assert timer.in_progress
|
||||
assert not timer.is_ready()
|
||||
|
||||
@@ -79,6 +79,7 @@ async def test_add_number_for_central_config(
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
| temps,
|
||||
)
|
||||
@@ -156,6 +157,7 @@ async def test_add_number_for_central_config_without_temp(
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
@@ -250,6 +252,7 @@ async def test_add_number_for_central_config_without_temp_ac_mode(
|
||||
CONF_AC_MODE: True,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
@@ -343,6 +346,7 @@ async def test_add_number_for_central_config_without_temp_restore(
|
||||
CONF_AC_MODE: False,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
@@ -441,6 +445,7 @@ async def test_add_number_for_over_switch_use_central(
|
||||
CONF_AC_MODE: False,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
@@ -666,6 +671,7 @@ async def test_add_number_for_over_switch_use_central_presets_and_restore(
|
||||
CONF_AC_MODE: False,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
@@ -788,6 +794,7 @@ async def test_change_central_config_temperature(
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_VALVE: "switch.mock_valve",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
@@ -823,6 +830,7 @@ async def test_change_central_config_temperature(
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_VALVE: "switch.mock_valve",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
@@ -905,6 +913,7 @@ async def test_change_vtherm_temperature(
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_VALVE: "switch.mock_valve",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
@@ -939,6 +948,7 @@ async def test_change_vtherm_temperature(
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_VALVE: "switch.mock_valve",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
@@ -1022,6 +1032,7 @@ async def test_change_vtherm_temperature_with_presence(
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_VALVE: "switch.mock_valve",
|
||||
@@ -1063,6 +1074,7 @@ async def test_change_vtherm_temperature_with_presence(
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_VALVE: "switch.mock_valve",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
|
||||
from custom_components.versatile_thermostat.prop_algorithm import (
|
||||
PropAlgorithm,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@@ -121,3 +124,156 @@ async def test_tpi_calculation(
|
||||
assert tpi_algo.calculated_on_percent == 0
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
"""
|
||||
Test the max_on_percent clamping calculations
|
||||
"""
|
||||
tpi_algo._max_on_percent = 0.8
|
||||
|
||||
# no clamping
|
||||
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 0.09
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
# no clamping (calculated_on_percent = 0.79)
|
||||
tpi_algo.calculate(15, 12.5, 11, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.79
|
||||
assert tpi_algo.calculated_on_percent == 0.79
|
||||
assert tpi_algo.on_time_sec == 237
|
||||
assert tpi_algo.off_time_sec == 63
|
||||
|
||||
# clamping to 80% (calculated_on_percent = 1)
|
||||
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.8 # should be clamped to 80%
|
||||
assert tpi_algo.calculated_on_percent == 1 # calculated percentage should not be affected by clamping
|
||||
assert tpi_algo.on_time_sec == 240 # capped at 80%
|
||||
assert tpi_algo.off_time_sec == 60
|
||||
|
||||
# clamping to 80% (calculated_on_percent = 0.81)
|
||||
tpi_algo.calculate(15, 12.5, 9, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.80 # should be clamped to 80%
|
||||
assert tpi_algo.calculated_on_percent == 0.81 # calculated percentage should not be affected by clamping
|
||||
assert tpi_algo.on_time_sec == 240 # capped at 80%
|
||||
assert tpi_algo.off_time_sec == 60
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_wrong_tpi_parameters(
|
||||
hass: HomeAssistant, skip_hass_states_is_state: None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the wrong TPI parameters"""
|
||||
|
||||
# Nominal case
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
0.6,
|
||||
0.01,
|
||||
5,
|
||||
1,
|
||||
"entity_id",
|
||||
)
|
||||
# We should not be there
|
||||
assert True
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
assert False
|
||||
|
||||
# Test TPI function
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
"WRONG",
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
"entity_id",
|
||||
)
|
||||
# We should not be there
|
||||
assert False
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
pass
|
||||
|
||||
# Test coef_int
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
None,
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
"entity_id",
|
||||
)
|
||||
# We should not be there
|
||||
assert False
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
pass
|
||||
|
||||
# Test coef_ext
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
0.6,
|
||||
None,
|
||||
2,
|
||||
3,
|
||||
"entity_id",
|
||||
)
|
||||
# We should not be there
|
||||
assert False
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
pass
|
||||
|
||||
# Test cycle_min
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
0.6,
|
||||
0.00001,
|
||||
None,
|
||||
3,
|
||||
"entity_id",
|
||||
)
|
||||
# We should not be there
|
||||
assert False
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
pass
|
||||
|
||||
# Test minimal_activation_delay
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
0.6,
|
||||
0.00001,
|
||||
0,
|
||||
None,
|
||||
"entity_id",
|
||||
)
|
||||
# We should not be there
|
||||
assert False
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
pass
|
||||
|
||||
# Test vtherm_entity_id
|
||||
try:
|
||||
algo = PropAlgorithm(
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
0.6,
|
||||
0.00001,
|
||||
0,
|
||||
12,
|
||||
None,
|
||||
)
|
||||
# We should not be there
|
||||
assert False
|
||||
except TypeError as e:
|
||||
# the normal case
|
||||
pass
|
||||
|
||||
@@ -6,10 +6,6 @@ from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
@@ -74,22 +70,12 @@ async def test_over_valve_full_start(
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 1. create the entity
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
# The name is in the CONF and not the title of the entry
|
||||
entity: ThermostatOverValve = find_my_entity("climate.theovervalvemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theovervalvemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverValve)
|
||||
@@ -130,7 +116,7 @@ async def test_over_valve_full_start(
|
||||
]
|
||||
)
|
||||
|
||||
# Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature
|
||||
# 2. Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -158,7 +144,7 @@ async def test_over_valve_full_start(
|
||||
# Nothing have changed cause we don't have room and external temperature
|
||||
assert mock_send_event.call_count == 1
|
||||
|
||||
# Set temperature and external temperature
|
||||
# 3. Set temperature and external temperature
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -181,14 +167,18 @@ async def test_over_valve_full_start(
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 90},
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 90},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
# {"entity_id": "number.mock_valve", "value": 90},
|
||||
),
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 98},
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 98},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
# {"entity_id": "number.mock_valve", "value": 98},
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -216,7 +206,7 @@ async def test_over_valve_full_start(
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Change internal temperature
|
||||
# 4. Change internal temperature
|
||||
expected_state = State(
|
||||
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
|
||||
)
|
||||
@@ -241,9 +231,10 @@ async def test_over_valve_full_start(
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 10},
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 10},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -254,20 +245,18 @@ async def test_over_valve_full_start(
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": "number.mock_valve",
|
||||
"value": 10,
|
||||
}, # the min allowed value
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 10},
|
||||
target={"entity_id": "number.mock_valve"}, # the min allowed value
|
||||
),
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": "number.mock_valve",
|
||||
"value": 50,
|
||||
}, # the max allowed value
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={
|
||||
"value": 34
|
||||
}, # 34 is 50 x open_percent (69%) and is the max allowed value
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -286,9 +275,9 @@ async def test_over_valve_full_start(
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0)
|
||||
# 5. Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0)
|
||||
expected_state = State(
|
||||
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99}
|
||||
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 255}
|
||||
)
|
||||
|
||||
with patch(
|
||||
@@ -466,9 +455,10 @@ async def test_over_valve_regulation(
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 90},
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 90},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -524,9 +514,10 @@ async def test_over_valve_regulation(
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 96},
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 96},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -556,3 +547,168 @@ async def test_over_valve_regulation(
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_533(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that with an over_valve and _auto_regulation_dpercent is set that the valve could close totally"""
|
||||
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# The temperatures to set
|
||||
temps = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverValveMockName",
|
||||
unique_id="overValveUniqueId",
|
||||
data={
|
||||
CONF_NAME: "overValve",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_VALVE: "number.mock_valve",
|
||||
CONF_AUTO_REGULATION_DTEMP: 10, # This parameter makes the bug
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 60,
|
||||
},
|
||||
# | temps,
|
||||
)
|
||||
|
||||
# Not used because number is not registred so we can use directly the underlying number
|
||||
# fake_underlying_number = MockNumber(
|
||||
# hass=hass, unique_id="mock_number", name="mock_number"
|
||||
# )
|
||||
|
||||
vtherm: ThermostatOverValve = await create_thermostat(
|
||||
hass, config_entry, "climate.overvalve"
|
||||
)
|
||||
|
||||
assert vtherm is not None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# Set all temps and check they are correctly initialized
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overvalve")
|
||||
await send_temperature_change_event(vtherm, 15, now)
|
||||
await send_ext_temperature_change_event(vtherm, 15, now)
|
||||
|
||||
# 1. Set mode to Heat and preset to Comfort
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="100",
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 19.0
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 100},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 2. set current temperature to 18 -> still 50% open, so there is a call
|
||||
now = now + timedelta(minutes=1)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="100",
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(vtherm, 18, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 50},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 3. set current temperature to 18.8 -> still 10% open, so there is one call
|
||||
now = now + timedelta(minutes=1)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="50",
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(vtherm, 18.8, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 10},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 4. set current temperature to 19 -> should have 0% open and one call to set the 0
|
||||
now = now + timedelta(minutes=1)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="10", # the previous value
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(vtherm, 19, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 0},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -205,6 +205,8 @@ async def test_window_management_time_enough(
|
||||
assert mock_heater_off.call_count == 2
|
||||
assert mock_condition.call_count == 1
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity._saved_hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window
|
||||
@@ -242,6 +244,9 @@ async def test_window_management_time_enough(
|
||||
any_order=False,
|
||||
)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity._saved_hvac_mode is HVACMode.HEAT # No change
|
||||
assert entity.hvac_off_reason == None
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
@@ -1339,6 +1344,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
|
||||
# The underlying should be in FAN_ONLY hvac_mode
|
||||
assert entity.hvac_mode is HVACMode.FAN_ONLY
|
||||
assert entity._saved_hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_off_reason is None # Hvac is not off
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 3. Close the window
|
||||
@@ -1357,7 +1363,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
|
||||
await try_function(None)
|
||||
|
||||
# Wait for initial delay of heater
|
||||
await asyncio.sleep(0.3)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert mock_send_event.call_count == 1
|
||||
@@ -1379,6 +1385,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
|
||||
)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity.hvac_off_reason is None
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
@@ -1925,3 +1932,162 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to open/close the window rapidly without side effect"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Open the window and let the thermostat shut down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window but too shortly
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should not have change
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Reopen immediatly with sufficient time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# still no change
|
||||
assert entity.window_state == STATE_ON
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
# Close the window but with sufficient time this time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should be Off this time and old state should have been restored
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
Reference in New Issue
Block a user