Compare commits

..

10 Commits

Author SHA1 Message Date
Jean-Marc Collin
7476e7fa64 Feature 158 central mode (#309)
* Normal algo working and testu ok

* Fix interaction with window

* FIX complex scenario

* pylint warning

* Release

* Issue #306

* Issue #306

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-03 17:52:34 +01:00
Jean-Marc Collin
c222feda1a Issue #295 - No floating point value for target temp 2024-01-01 16:55:41 +00:00
Jean-Marc Collin
d05df021ab Beer from Lajull 2023-12-24 09:07:59 +00:00
Jean-Marc Collin
27a267139f FIX #159 - Doesn't send target temp if VTherm is off 2023-12-20 19:06:34 +00:00
Jean-Marc Collin
707f40d406 FIX issue #284 - preset not saved 2023-12-20 18:54:35 +00:00
Jean-Marc Collin
a01f5770d9 FIX issue #272 and #24ç - min and max values depending of the underlying 2023-12-19 19:39:33 +00:00
Jean-Marc Collin
04d0b28f1d Issue #280 - enable to use central config for window configuration 2023-12-18 21:39:58 +00:00
Jean-Marc Collin
30c3418f1b Issue #281 - cannot use central config at first integration installation 2023-12-18 20:54:39 +00:00
Jean-Marc Collin
efb8ce257d Beers from @Mexx62, @Someone 2023-12-18 20:16:37 +00:00
Jean-Marc Collin
8f934a3298 Feature 239 creates central config panel (#276)
* Add central config into ConfigFlow

* Test manual of confif_flow ok

* Ignore central confic in instanciate entities

* Init data in base_thermostat ok

* With central configuration testu ok

* All testu ok

* With fixture for init_vtherm_api and init_central_config

* Add reload VTherms when central configuration is changed

* Update strings.json and replace security by safety in README.

* UPdate README with release 5.0

* FIX missing Presets central configuration initialisation

* FIX frost_away_temp translation missing

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-12-17 16:16:44 +01:00
35 changed files with 1898 additions and 185 deletions

View File

@@ -1,5 +1,8 @@
default_config: default_config:
# ffmeg
ffmpeg:
logger: logger:
default: info default: info
logs: logs:
@@ -59,8 +62,8 @@ input_number:
unit_of_measurement: kW unit_of_measurement: kW
fake_valve1: fake_valve1:
name: The valve 1 name: The valve 1
min: 0 min: 10
max: 100 max: 90
icon: mdi:pipe-valve icon: mdi:pipe-valve
unit_of_measurement: percentage unit_of_measurement: percentage

View File

@@ -30,13 +30,8 @@
"waderyan.gitblame", "waderyan.gitblame",
"keesschollaart.vscode-home-assistant", "keesschollaart.vscode-home-assistant",
"vscode.markdown-math", "vscode.markdown-math",
"yzhang.markdown-all-in-one", "yzhang.markdown-all-in-one"
"ms-python.vscode-pylance"
], ],
// "mounts": [
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
// ],
"settings": { "settings": {
"files.eol": "\n", "files.eol": "\n",
"editor.tabSize": 4, "editor.tabSize": 4,

View File

@@ -14,7 +14,8 @@
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
// "/home/vscode/core", // "/home/vscode/core",
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat" "/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
], ],
"python.formatting.provider": "none" "python.formatting.provider": "none"
} }

View File

@@ -34,6 +34,7 @@
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance) - [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation) - [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
- [Configuration avancée](#configuration-avancée) - [Configuration avancée](#configuration-avancée)
- [Le contrôle centralisé](#le-contrôle-centralisé)
- [Synthèse des paramètres](#synthèse-des-paramètres) - [Synthèse des paramètres](#synthèse-des-paramètres)
- [Exemples de réglage](#exemples-de-réglage) - [Exemples de réglage](#exemples-de-réglage)
- [Chauffage électrique](#chauffage-électrique) - [Chauffage électrique](#chauffage-électrique)
@@ -74,14 +75,16 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_ > ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **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 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.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.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).
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
<details> <details>
<summary>Autres versions</summary> <summary>Autres versions</summary>
> * **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).
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124). > * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque > * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127) > * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
@@ -114,7 +117,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour. **Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour.
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # 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 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 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
# Quand l'utiliser et ne pas l'utiliser # Quand l'utiliser et ne pas l'utiliser
@@ -150,7 +153,8 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré. - Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
- La **gestion de la présence à domicile**. Cette fonctionnalité vous permet de modifier dynamiquement la température du préréglage en tenant compte d'un capteur de présence de votre maison. - La **gestion de la présence à domicile**. Cette fonctionnalité vous permet de modifier dynamiquement la température du préréglage en tenant compte d'un capteur de présence de votre maison.
- Des **services pour interagir avec le thermostat** à partir d'autres intégrations : vous pouvez forcer la présence / la non-présence à l'aide d'un service, et vous pouvez modifier dynamiquement la température des préréglages et changer les paramètres de sécurité. - Des **services pour interagir avec le thermostat** à partir d'autres intégrations : vous pouvez forcer la présence / la non-présence à l'aide d'un service, et vous pouvez modifier dynamiquement la température des préréglages et changer les paramètres de sécurité.
- Ajouter des capteurs pour voir les états internes du thermostat. - Ajouter des capteurs pour voir les états internes du thermostat,
- Contrôle centralisé de tous les Versatile Thermostat pour les stopper tous, les passer tous en hors-gel, les forcer en mode Chauffage (l'hiver), les forcer en mode Climatisation (l'été).
# Comment installer cet incroyable Thermostat Versatile ? # Comment installer cet incroyable Thermostat Versatile ?
@@ -187,7 +191,9 @@ Suivez ensuite les étapes de configuration comme suit :
## Choix des attributs de base ## Choix des attributs de base
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true) ![image](/images/config-main0.png?raw=true)
![image](/images/config-main.png?raw=true)
Donnez les principaux attributs obligatoires : Donnez les principaux attributs obligatoires :
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate) 1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
@@ -197,7 +203,8 @@ Donnez les principaux attributs obligatoires :
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait, 6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait,
7. les températures minimales et maximales du thermostat, 7. les températures minimales et maximales du thermostat,
8. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil, 8. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil,
9. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas. 9. la possibilité de controler le thermostat de façon centralisée. Cf [controle centralisé](#le-contrôle-centralisé),
10. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. avec les types ```over_switch``` et ```over_valve```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**, > 1. avec les types ```over_switch``` et ```over_valve```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
@@ -515,6 +522,21 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``, > 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security. > 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
## Le contrôle centralisé
Depuis la release 5.2, si vous avez défini une configuration centralisée, vous avez une nouvelle entité nommée `select.central_mode` qui permet de piloter tous les VTherms avec une seule action. Pour qu'un VTherm soit contrôlable de façon centralisée, il faut que son attribut de configuration nommé `use_central_mode` soit vrai.
Cette entité se présente sous la forme d'une liste de choix qui contient les choix suivants :
1. `Auto` : le mode 'normal' dans lequel chaque VTherm se comporte comme dans les versions précédentes,
2. `Stooped` : tous les VTherms sont mis à l'arrêt (`hvac_off`),
3. `Heat only` : tous les VTherms sont mis en mode chauffage lorsque ce mode est supporté par le VTherm, sinon il est stoppé,
3. `Cool only` : tous les VTherms sont mis en mode climatisation lorsque ce mode est supporté par le VTherm, sinon il est stoppé,
4. `Frost protection` : tous les VTherms sont mis en preset hors-gel lorsque ce preset est supporté par le VTherm, sinon il est stoppé.
Il est donc possible de contrôler tous les VTherms (que ceux que l'on désigne explicitement) avec un seul contrôle.
Exemple de rendu :
![central_mode](/images/central_mode.png?raw=true)
## Synthèse des paramètres ## Synthèse des paramètres
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "configuration centrale" | | Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "configuration centrale" |
@@ -527,6 +549,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``temp_min`` | Température minimale permise | X | X | X | X | | ``temp_min`` | Température minimale permise | X | X | X | X |
| ``temp_max`` | Température maximale permise | X | X | X | X | | ``temp_max`` | Température maximale permise | X | X | X | X |
| ``device_power`` | Puissance de l'équipement | X | X | X | - | | ``device_power`` | Puissance de l'équipement | X | X | X | - |
| ``use_central_mode`` | Autorisation du contrôle centralisé | X | X | X | - |
| ``use_window_feature`` | Avec détection des ouvertures | X | X | X | - | | ``use_window_feature`` | Avec détection des ouvertures | X | X | X | - |
| ``use_motion_feature`` | Avec détection de mouvement | X | X | X | - | | ``use_motion_feature`` | Avec détection de mouvement | X | X | X | - |
| ``use_power_feature`` | Avec gestion de la puissance | X | X | X | - | | ``use_power_feature`` | Avec gestion de la puissance | X | X | X | - |
@@ -835,6 +858,8 @@ Les attributs personnalisés sont les suivants :
| ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne | | ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne |
| ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation | | ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation |
| ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) | | ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) |
| ``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) |
# Quelques résultats # Quelques résultats
@@ -1008,6 +1033,10 @@ Remplacez les valeurs entre [[ ]] par les votres.
step: day step: day
``` ```
Exemple de courbes obtenues avec Plotly :
![image](/images/plotly-curves.png?raw=true)
## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements ## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements
Cette automatisation utilise l'excellente App Daemon nommée NOTIFIER développée par Horizon Domotique que vous trouverez en démonstration [ici](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) et le code est [ici](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). Elle permet de notifier les utilisateurs du logement lorsqu'un des évènements touchant à la sécurité survient sur un des Versatile Thermostats. Cette automatisation utilise l'excellente App Daemon nommée NOTIFIER développée par Horizon Domotique que vous trouverez en démonstration [ici](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) et le code est [ici](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). Elle permet de notifier les utilisateurs du logement lorsqu'un des évènements touchant à la sécurité survient sur un des Versatile Thermostats.

View File

@@ -34,6 +34,7 @@
- [Configure the power management](#configure-the-power-management) - [Configure the power management](#configure-the-power-management)
- [Configure presence or occupancy](#configure-presence-or-occupancy) - [Configure presence or occupancy](#configure-presence-or-occupancy)
- [Advanced configuration](#advanced-configuration) - [Advanced configuration](#advanced-configuration)
- [Centralized control](#centralized-control)
- [Parameters synthesis](#parameters-synthesis) - [Parameters synthesis](#parameters-synthesis)
- [Examples tuning](#examples-tuning) - [Examples tuning](#examples-tuning)
- [Electrical heater](#electrical-heater) - [Electrical heater](#electrical-heater)
@@ -74,16 +75,18 @@
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. This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_ >![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **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). > * **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).
> * **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.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.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.
<details>
<summary>Others releases</summary>
> * **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). > * **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).
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144) > * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
> * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124). > * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language > * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
<details>
<summary>Others releases</summary>
> * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127) > * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113) > * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103) > * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
@@ -114,7 +117,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
**Note:** the VTherm configuration screenshots have not been updated. **Note:** the VTherm configuration screenshots have not been updated.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # 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 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 for the beers. It's very nice and encourages me to continue!
# When to use / not use # When to use / not use
This thermostat can control 3 types of equipment: This thermostat can control 3 types of equipment:
@@ -150,7 +153,8 @@ This component named __Versatile thermostat__ manage the following use cases :
- Add **power shedding management** or regulation to avoid exceeding a defined total power. When max power is exceeded, a hidden 'power' preset is set on the climate entity. When power goes below the max, the previous preset is restored. - Add **power shedding management** or regulation to avoid exceeding a defined total power. When max power is exceeded, a hidden 'power' preset is set on the climate entity. When power goes below the max, the previous preset is restored.
- Add **home presence management**. This feature allows you to dynamically change the temperature of preset considering a occupancy sensor of your home. - Add **home presence management**. This feature allows you to dynamically change the temperature of preset considering a occupancy sensor of your home.
- Add **services to interact with the thermostat** from others integration: you can force the presence / un-presence using a service, and you can dynamically change the temperature of the presets and change dynamically the safety parameters. - Add **services to interact with the thermostat** from others integration: you can force the presence / un-presence using a service, and you can dynamically change the temperature of the presets and change dynamically the safety parameters.
- Add sensors to see the internal states of the thermostat - Add sensors to see the internal states of the thermostat,
- Centralized control of all Versatile Thermostats to stop them all, switch them all to frost protection, force them into Heating mode (winter), force them into Cooling mode (summer).
# How to install this incredible Versatile Thermostat ? # How to install this incredible Versatile Thermostat ?
@@ -185,7 +189,10 @@ The configuration can be change through the same interface. Simply select the th
Then follow the configurations steps as follow: Then follow the configurations steps as follow:
## Minimal configuration update ## Minimal configuration update
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true)
![image](/images/config-main0.png?raw=true)
![image](/images/config-main.png?raw=true)
Give the main mandatory attributes: Give the main mandatory attributes:
1. a name (will be the name of the integration and also the name of the climate entity) 1. a name (will be the name of the integration and also the name of the climate entity)
@@ -195,7 +202,8 @@ Give the main mandatory attributes:
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it, 6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it,
7. minimum and maximum thermostat temperatures, 7. minimum and maximum thermostat temperatures,
8. the power of the l'équipement which will activate the power and energy sensors of the device, 8. the power of the l'équipement which will activate the power and energy sensors of the device,
9. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not. 9. the possibility of controlling the thermostat centrally. Cf [centralized control](#centralized-control),
10. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**, > 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
@@ -500,6 +508,21 @@ See [example tuning](#examples-tuning) for common tuning examples
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``, > 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature. > 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature.
## 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.
This entity is presented in the form of a list of choices which contains the following choices:
1. `Auto`: the 'normal' mode in which each VTherm behaves as in previous versions,
2. `Stooped`: all VTherms are turned off (`hvac_off`),
3. `Heat only`: all VTherms are put in heating mode when this mode is supported by the VTherm, otherwise it is stopped,
3. `Cool only`: all VTherms are put in cooling mode when this mode is supported by the VTherm, otherwise it is stopped,
4. `Frost protection`: all VTherms are put in frost protection preset when this preset is supported by the VTherm, otherwise it is stopped.
It is therefore possible to control all VTherms (only those explicitly designated) with a single control.
Example rendering:
![central_mode](/images/central_mode.png?raw=true)
## Parameters synthesis ## Parameters synthesis
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "central configuration" | | Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "central configuration" |
@@ -512,6 +535,7 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``temp_min`` | Minimal temperature allowed | X | X | X | X | | ``temp_min`` | Minimal temperature allowed | X | X | X | X |
| ``temp_max`` | Maximal temperature allowed | X | X | X | X | | ``temp_max`` | Maximal temperature allowed | X | X | X | X |
| ``device_power`` | Total device power | X | X | X | - | | ``device_power`` | Total device power | X | X | X | - |
| ``use_central_mode`` | Allow the centralized control | X | X | X | - |
| ``use_window_feature`` | Use window detection | X | X | X | - | | ``use_window_feature`` | Use window detection | X | X | X | - |
| ``use_motion_feature`` | Use motion detection | X | X | X | - | | ``use_motion_feature`` | Use motion detection | X | X | X | - |
| ``use_power_feature`` | Use power management | X | X | X | - | | ``use_power_feature`` | Use power management | X | X | X | - |
@@ -819,6 +843,8 @@ Custom attributes are the following:
| ``valve_open_percent`` | The opening percentage of the valve | | ``valve_open_percent`` | The opening percentage of the valve |
| ``regulated_target_temperature`` | The self-regulated target temperature calculated | | ``regulated_target_temperature`` | The self-regulated target temperature calculated |
| ``is_inversed`` | True if the command is inversed (pilot wire with diode) | | ``is_inversed`` | True if the command is inversed (pilot wire with diode) |
| ``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) |
# Some results # Some results
@@ -991,6 +1017,11 @@ Replace values in [[ ]] by yours.
step: day step: day
``` ```
Example of graph obtained with Plotly :
![image](/images/plotly-curves.png?raw=true)
## And always better and better with the NOTIFIER daemon app to notify events ## And always better and better with the NOTIFIER daemon app to notify events
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats. This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.

View File

@@ -116,6 +116,11 @@ from .const import (
ATTR_TOTAL_ENERGY, ATTR_TOTAL_ENERGY,
PRESET_AC_SUFFIX, PRESET_AC_SUFFIX,
DEFAULT_SHORT_EMA_PARAMS, DEFAULT_SHORT_EMA_PARAMS,
CENTRAL_MODE_AUTO,
CENTRAL_MODE_STOPPED,
CENTRAL_MODE_HEAT_ONLY,
CENTRAL_MODE_COOL_ONLY,
CENTRAL_MODE_FROST_PROTECTION,
) )
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -158,6 +163,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
frozenset( frozenset(
{ {
"is_on", "is_on",
"is_controlled_by_central_mode",
"last_central_mode",
"type", "type",
"frost_temp", "frost_temp",
"eco_temp", "eco_temp",
@@ -273,6 +280,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._now = None self._now = None
self._attr_fan_mode = None self._attr_fan_mode = None
self._is_central_mode = None
self._last_central_mode = None
self.post_init(entry_infos) self.post_init(entry_infos)
def clean_central_config_doublon(self, config_entry, central_config) -> dict: def clean_central_config_doublon(self, config_entry, central_config) -> dict:
@@ -434,6 +444,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._presence_on = self._presence_sensor_entity_id is not None self._presence_on = self._presence_sensor_entity_id is not None
if self._ac_mode: if self._ac_mode:
# Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
# Some over_switch can do both heating and cooling
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
else: else:
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
@@ -552,6 +564,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
short_ema_params.get("max_alpha"), short_ema_params.get("max_alpha"),
) )
self._is_central_mode = not (
entry_infos.get(CONF_USE_CENTRAL_MODE) is False
) # Default value (None) is True
_LOGGER.debug( _LOGGER.debug(
"%s - Creation of a new VersatileThermostat entity: unique_id=%s", "%s - Creation of a new VersatileThermostat entity: unique_id=%s",
self, self,
@@ -1130,6 +1146,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""True if the VTherm is on (! HVAC_OFF)""" """True if the VTherm is on (! HVAC_OFF)"""
return self.hvac_mode and self.hvac_mode != HVACMode.OFF return self.hvac_mode and self.hvac_mode != HVACMode.OFF
@property
def is_controlled_by_central_mode(self) -> bool:
"""Returns True if this VTherm can be controlled by the central_mode"""
return self._is_central_mode
@property
def last_central_mode(self) -> str | None:
"""Returns the last central_mode taken into account.
Is None if the VTherm is not controlled by central_mode"""
return self._last_central_mode
def underlying_entity_id(self, index=0) -> str | None: def underlying_entity_id(self, index=0) -> str | None:
"""The climate_entity_id. Added for retrocompatibility reason""" """The climate_entity_id. Added for retrocompatibility reason"""
if index < self.nb_underlying_entities: if index < self.nb_underlying_entities:
@@ -1177,11 +1204,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
) )
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode) # If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
if self._ac_mode: if self._hvac_mode == HVACMode.COOL:
if self.preset_mode != PRESET_FROST_PROTECTION: if self.preset_mode != PRESET_FROST_PROTECTION:
await self._async_set_preset_mode_internal(self._attr_preset_mode, True) await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
else: else:
await self._async_set_preset_mode_internal(PRESET_ECO, True) await self._async_set_preset_mode_internal(PRESET_ECO, True, False)
if need_control_heating and sub_need_control_heating: if need_control_heating and sub_need_control_heating:
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
@@ -1195,12 +1222,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.async_write_ha_state() self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
async def async_set_preset_mode(self, preset_mode): @overrides
async def async_set_preset_mode(self, preset_mode, overwrite_saved_preset=True):
"""Set new preset mode.""" """Set new preset mode."""
await self._async_set_preset_mode_internal(preset_mode) await self._async_set_preset_mode_internal(
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
)
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_set_preset_mode_internal(self, preset_mode, force=False): async def _async_set_preset_mode_internal(
self, preset_mode, force=False, overwrite_saved_preset=True
):
"""Set new preset mode.""" """Set new preset mode."""
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force) _LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
if ( if (
@@ -1215,10 +1247,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
# I don't think we need to call async_write_ha_state if we didn't change the state # I don't think we need to call async_write_ha_state if we didn't change the state
return return
# In security mode don't change preset but memorise the new expected preset when security will be off # In safety mode don't change preset but memorise the new expected preset when security will be off
if preset_mode != PRESET_SECURITY and self._security_state: if preset_mode != PRESET_SECURITY and self._security_state:
_LOGGER.debug( _LOGGER.debug(
"%s - is in security mode. Just memorise the new expected ", self "%s - is in safety mode. Just memorise the new expected ", self
) )
if preset_mode not in HIDDEN_PRESETS: if preset_mode not in HIDDEN_PRESETS:
self._saved_preset_mode = preset_mode self._saved_preset_mode = preset_mode
@@ -1242,7 +1274,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.reset_last_temperature_time(old_preset_mode) self.reset_last_temperature_time(old_preset_mode)
self.save_preset_mode() if overwrite_saved_preset:
self.save_preset_mode()
self.recalculate() self.recalculate()
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode}) self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
@@ -1442,16 +1475,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
else: else:
if not self._window_state: if not self._window_state:
_LOGGER.info( _LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s'", "%s - Window is closed. Restoring hvac_mode '%s' if central_mode is not STOPPED",
self, self,
self._saved_hvac_mode, self._saved_hvac_mode,
) )
await self.restore_hvac_mode(True) if self.last_central_mode != CENTRAL_MODE_STOPPED:
await self.restore_hvac_mode(True)
elif self._window_state: elif self._window_state:
_LOGGER.info( _LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF "%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
) )
self.save_hvac_mode() if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
self.update_custom_attributes() self.update_custom_attributes()
@@ -1604,7 +1640,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
state.last_changed.astimezone(self._current_tz), state.last_changed.astimezone(self._current_tz),
) )
# try to restart if we were in security mode # try to restart if we were in safety mode
if self._security_state: if self._security_state:
await self.check_security() await self.check_security()
@@ -1631,7 +1667,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
state.last_changed.astimezone(self._current_tz), state.last_changed.astimezone(self._current_tz),
) )
# try to restart if we were in security mode # try to restart if we were in safety mode
if self._security_state: if self._security_state:
await self.check_security() await self.check_security()
except ValueError as ex: except ValueError as ex:
@@ -1998,6 +2034,67 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return self._overpowering_state return self._overpowering_state
async def check_central_mode(self, new_central_mode, old_central_mode) -> None:
"""Take into account a central mode change"""
if not self.is_controlled_by_central_mode:
self._last_central_mode = None
return
_LOGGER.info(
"%s - Central mode have change from %s to %s",
self,
old_central_mode,
new_central_mode,
)
self._last_central_mode = new_central_mode
def save_all():
"""save preset and hvac_mode"""
self.save_preset_mode()
self.save_hvac_mode()
if new_central_mode == CENTRAL_MODE_AUTO:
if self.window_state is not STATE_ON:
await self.restore_hvac_mode()
await self.restore_preset_mode()
return
if old_central_mode == CENTRAL_MODE_AUTO and self.window_state is not STATE_ON:
save_all()
if new_central_mode == CENTRAL_MODE_STOPPED:
await self.async_set_hvac_mode(HVACMode.OFF)
return
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
if HVACMode.COOL in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.COOL)
else:
await self.async_set_hvac_mode(HVACMode.OFF)
return
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
if HVACMode.HEAT in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.HEAT)
else:
await self.async_set_hvac_mode(HVACMode.OFF)
return
if new_central_mode == CENTRAL_MODE_FROST_PROTECTION:
if (
PRESET_FROST_PROTECTION in self.preset_modes
and HVACMode.HEAT in self.hvac_modes
):
await self.async_set_hvac_mode(HVACMode.HEAT)
await self.async_set_preset_mode(
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
)
else:
await self.async_set_hvac_mode(HVACMode.OFF)
return
def _set_now(self, now: datetime): def _set_now(self, now: datetime):
"""Set the now timestamp. This is only for tests purpose""" """Set the now timestamp. This is only for tests purpose"""
self._now = now self._now = now
@@ -2064,7 +2161,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if shouldStartSecurity: if shouldStartSecurity:
if shouldClimateBeInSecurity: if shouldClimateBeInSecurity:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Setting it into safety mode",
self, self,
self._security_delay_min, self._security_delay_min,
delta_temp, delta_temp,
@@ -2073,13 +2170,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
) )
elif shouldSwitchBeInSecurity: elif shouldSwitchBeInSecurity:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f %%) is over defined value (%.2f %%). Set it into safety mode",
self, self,
self._security_delay_min, self._security_delay_min,
delta_temp, delta_temp,
delta_ext_temp, delta_ext_temp,
self._prop_algorithm.on_percent, self._prop_algorithm.on_percent * 100,
self._security_min_on_percent, self._security_min_on_percent * 100,
) )
self.send_event( self.send_event(
@@ -2097,7 +2194,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
}, },
) )
# Start security mode # Start safety mode
if shouldStartSecurity: if shouldStartSecurity:
self._security_state = True self._security_state = True
self.save_hvac_mode() self.save_hvac_mode()
@@ -2125,10 +2222,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
}, },
) )
# Stop security mode # Stop safety mode
if shouldStopSecurity: if shouldStopSecurity:
_LOGGER.warning( _LOGGER.warning(
"%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s", "%s - End of safety mode. restoring hvac_mode to %s and preset_mode to %s",
self, self,
self._saved_hvac_mode, self._saved_hvac_mode,
self._saved_preset_mode, self._saved_preset_mode,
@@ -2239,6 +2336,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"hvac_mode": self.hvac_mode, "hvac_mode": self.hvac_mode,
"preset_mode": self.preset_mode, "preset_mode": self.preset_mode,
"type": self._thermostat_type, "type": self._thermostat_type,
"is_controlled_by_central_mode": self.is_controlled_by_central_mode,
"last_central_mode": self.last_central_mode,
"frost_temp": self._presets[PRESET_FROST_PROTECTION], "frost_temp": self._presets[PRESET_FROST_PROTECTION],
"eco_temp": self._presets[PRESET_ECO], "eco_temp": self._presets[PRESET_ECO],
"boost_temp": self._presets[PRESET_BOOST], "boost_temp": self._presets[PRESET_BOOST],
@@ -2374,11 +2473,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
entity_id: climate.thermostat_2 entity_id: climate.thermostat_2
""" """
_LOGGER.info( _LOGGER.info(
"%s - Calling service_set_security, delay_min: %s, min_on_percent: %s, default_on_percent: %s", "%s - Calling service_set_security, delay_min: %s, min_on_percent: %s %%, default_on_percent: %s %%",
self, self,
delay_min, delay_min,
min_on_percent, min_on_percent*100,
default_on_percent, default_on_percent*100,
) )
if delay_min: if delay_min:
self._security_delay_min = delay_min self._security_delay_min = delay_min

View File

@@ -86,7 +86,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
# VTherm API should have been initialized before arriving here # VTherm API should have been initialized before arriving here
vtherm_api = VersatileThermostatAPI.get_vtherm_api() vtherm_api = VersatileThermostatAPI.get_vtherm_api()
self._central_config = vtherm_api.find_central_configuration() if vtherm_api is not None:
self._central_config = vtherm_api.find_central_configuration()
else:
self._central_config = None
self._init_feature_flags(infos) self._init_feature_flags(infos)
self._init_central_config_flags(infos) self._init_central_config_flags(infos)
@@ -119,6 +122,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_WINDOW_CENTRAL_CONFIG, CONF_USE_WINDOW_CENTRAL_CONFIG,
CONF_USE_MOTION_CENTRAL_CONFIG, CONF_USE_MOTION_CENTRAL_CONFIG,
CONF_USE_POWER_CENTRAL_CONFIG, CONF_USE_POWER_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
): ):
@@ -167,7 +171,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
_LOGGER.error( _LOGGER.error(
"Only one window detection method should be used. Use window_sensor or auto window open detection but not both" "Only one window detection method should be used. Use window_sensor or auto window open detection but not both"
) )
raise WindowOpenDetectionMethod(CONF_WINDOW_SENSOR) raise WindowOpenDetectionMethod(CONF_WINDOW_AUTO_OPEN_THRESHOLD)
# Check that is USE_CENTRAL config is used, that a central config exists # Check that is USE_CENTRAL config is used, that a central config exists
if self._central_config is None: if self._central_config is None:
@@ -360,7 +364,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
schema = schema_ac_or_not schema = schema_ac_or_not
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False: elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_presets next_step = self.async_step_spec_presets
schema = schema_ac_or_not schema = STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step) return await self.generic_step("presets", schema, user_input, next_step)
@@ -404,7 +408,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
next_step = self.async_step_motion next_step = self.async_step_motion
# If comes from async_step_spec_window # If comes from async_step_spec_window
elif self._infos.get(COMES_FROM) == "async_step_spec_window": elif self._infos.get(COMES_FROM) == "async_step_spec_window":
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA # If we have a window sensor don't display the auto window parameters
if self._infos.get(CONF_WINDOW_SENSOR) is not None:
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
else:
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False: elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_window next_step = self.async_step_spec_window
@@ -419,6 +427,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
) )
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
if self._infos.get(CONF_WINDOW_SENSOR) is not None:
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
self._infos[COMES_FROM] = "async_step_spec_window" self._infos[COMES_FROM] = "async_step_spec_window"

View File

@@ -42,6 +42,7 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
), ),
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int, vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float), vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean, vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_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_POWER_FEATURE, default=False): cv.boolean,
@@ -195,6 +196,12 @@ STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
} }
) )
STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
}
)
STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector( vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(

View File

@@ -35,7 +35,12 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat" DOMAIN = "versatile_thermostat"
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR] PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SELECT,
]
CONF_HEATER = "heater_entity_id" CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id" CONF_HEATER_2 = "heater_entity2_id"
@@ -113,6 +118,8 @@ CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config"
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config" CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config" CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
CONF_USE_CENTRAL_MODE = "use_central_mode"
DEFAULT_SHORT_EMA_PARAMS = { DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5, "max_alpha": 0.5,
# In sec # In sec
@@ -242,6 +249,7 @@ ALL_CONF = (
CONF_USE_POWER_CENTRAL_CONFIG, CONF_USE_POWER_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
] ]
+ CONF_PRESETS_VALUES + CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES + CONF_PRESETS_AWAY_VALUES
@@ -297,6 +305,19 @@ AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"]
CENTRAL_CONFIG_NAME = "Central configuration" CENTRAL_CONFIG_NAME = "Central configuration"
CENTRAL_MODE_AUTO = "Auto"
CENTRAL_MODE_STOPPED = "Stopped"
CENTRAL_MODE_HEAT_ONLY = "Heat only"
CENTRAL_MODE_COOL_ONLY = "Cool only"
CENTRAL_MODE_FROST_PROTECTION = "Frost protection"
CENTRAL_MODES = [
CENTRAL_MODE_AUTO,
CENTRAL_MODE_STOPPED,
CENTRAL_MODE_HEAT_ONLY,
CENTRAL_MODE_COOL_ONLY,
CENTRAL_MODE_FROST_PROTECTION,
]
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154 # A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
class RegulationParamSlow: class RegulationParamSlow:

View File

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

View File

@@ -140,27 +140,27 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
def set_security(self, default_on_percent: float): def set_security(self, default_on_percent: float):
"""Set a default value for on_percent (used for security mode)""" """Set a default value for on_percent (used for safety mode)"""
self._security = True self._security = True
self._default_on_percent = default_on_percent self._default_on_percent = default_on_percent
self._calculate_internal() self._calculate_internal()
def unset_security(self): def unset_security(self):
"""Unset the security mode""" """Unset the safety mode"""
self._security = False self._security = False
self._calculate_internal() self._calculate_internal()
@property @property
def on_percent(self) -> float: def on_percent(self) -> float:
"""Returns the percentage the heater must be ON """Returns the percentage the heater must be ON
In security mode this value is overriden with the _default_on_percent In safety mode this value is overriden with the _default_on_percent
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._on_percent, 2) return round(self._on_percent, 2)
@property @property
def calculated_on_percent(self) -> float: def calculated_on_percent(self) -> float:
"""Returns the calculated percentage the heater must be ON """Returns the calculated percentage the heater must be ON
Calculated means NOT overriden even in security mode Calculated means NOT overriden even in safety mode
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._calculated_on_percent, 2) return round(self._calculated_on_percent, 2)

View File

@@ -0,0 +1,134 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat select component """
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState, callback
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
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CENTRAL_MODE_AUTO,
CENTRAL_MODES,
overrides,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat selects 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)
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
return
entities = [
CentralModeSelect(hass, unique_id, name, entry.data),
]
async_add_entities(entities, True)
class CentralModeSelect(SelectEntity, RestoreEntity):
"""Representation of a Energy sensor which exposes the energy"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME)
self._attr_name = "Central Mode"
self._attr_unique_id = "central_mode"
self._attr_options = CENTRAL_MODES
self._attr_current_option = CENTRAL_MODE_AUTO
@property
def icon(self) -> str | None:
return "mdi:form-select"
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
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()
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:
"""Change the selected option."""
old_option = self._attr_current_option
if option == old_option:
return
if option in CENTRAL_MODES:
self._attr_current_option = option
await self.notify_central_mode_change(old_central_mode=old_option)
async def notify_central_mode_change(self, old_central_mode=None):
"""Notify all VTherm that the central_mode have change"""
# 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
)
def __str__(self):
return f"VersatileThermostat-{self.name}"

View File

@@ -24,6 +24,7 @@
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximal temperature allowed",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')",
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
@@ -31,6 +32,7 @@
"use_main_central_config": "Use central main configuration" "use_main_central_config": "Use central main configuration"
}, },
"data_description": { "data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
@@ -254,6 +256,7 @@
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximal temperature allowed",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')",
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
@@ -261,6 +264,7 @@
"use_main_central_config": "Use central main configuration" "use_main_central_config": "Use central main configuration"
}, },
"data_description": { "data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
@@ -413,6 +417,7 @@
"eco_away_temp": "Eco away preset", "eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset", "comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset", "boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode", "eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort away preset in AC mode", "comfort_ac_away_temp": "Comfort away preset in AC mode",
"boost_ac_away_temp": "Boost away preset in AC mode", "boost_ac_away_temp": "Boost away preset in AC mode",

View File

@@ -136,6 +136,11 @@ class ThermostatOverClimate(BaseThermostat):
async def _send_regulated_temperature(self, force=False): async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying""" """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 ")
return
_LOGGER.info( _LOGGER.info(
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s", "%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
self, self,
@@ -234,45 +239,45 @@ class ThermostatOverClimate(BaseThermostat):
await self.async_set_fan_mode(self._auto_deactivated_fan_mode) await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, config_entry):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(config_entry)
for climate in [ for climate in [
CONF_CLIMATE, CONF_CLIMATE,
CONF_CLIMATE_2, CONF_CLIMATE_2,
CONF_CLIMATE_3, CONF_CLIMATE_3,
CONF_CLIMATE_4, CONF_CLIMATE_4,
]: ]:
if entry_infos.get(climate): if config_entry.get(climate):
self._underlyings.append( self._underlyings.append(
UnderlyingClimate( UnderlyingClimate(
hass=self._hass, hass=self._hass,
thermostat=self, thermostat=self,
climate_entity_id=entry_infos.get(climate), climate_entity_id=config_entry.get(climate),
) )
) )
self.choose_auto_regulation_mode( self.choose_auto_regulation_mode(
entry_infos.get(CONF_AUTO_REGULATION_MODE) config_entry.get(CONF_AUTO_REGULATION_MODE)
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
else CONF_AUTO_REGULATION_NONE else CONF_AUTO_REGULATION_NONE
) )
self._auto_regulation_dtemp = ( self._auto_regulation_dtemp = (
entry_infos.get(CONF_AUTO_REGULATION_DTEMP) config_entry.get(CONF_AUTO_REGULATION_DTEMP)
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5 else 0.5
) )
self._auto_regulation_period_min = ( self._auto_regulation_period_min = (
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 5 else 5
) )
self._auto_fan_mode = ( self._auto_fan_mode = (
entry_infos.get(CONF_AUTO_FAN_MODE) config_entry.get(CONF_AUTO_FAN_MODE)
if entry_infos.get(CONF_AUTO_FAN_MODE) is not None if config_entry.get(CONF_AUTO_FAN_MODE) is not None
else CONF_AUTO_FAN_NONE else CONF_AUTO_FAN_NONE
) )

View File

@@ -48,9 +48,9 @@ class ThermostatOverSwitch(BaseThermostat):
) )
# useless for now # useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos) # super().__init__(hass, unique_id, name, config_entry)
_is_inversed: bool = None _is_inversed: bool = None
@property @property
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
return None return None
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, config_entry):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm( self._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
@@ -85,13 +85,13 @@ class ThermostatOverSwitch(BaseThermostat):
self._minimal_activation_delay, self._minimal_activation_delay,
) )
lst_switches = [entry_infos.get(CONF_HEATER)] lst_switches = [config_entry.get(CONF_HEATER)]
if entry_infos.get(CONF_HEATER_2): if config_entry.get(CONF_HEATER_2):
lst_switches.append(entry_infos.get(CONF_HEATER_2)) lst_switches.append(config_entry.get(CONF_HEATER_2))
if entry_infos.get(CONF_HEATER_3): if config_entry.get(CONF_HEATER_3):
lst_switches.append(entry_infos.get(CONF_HEATER_3)) lst_switches.append(config_entry.get(CONF_HEATER_3))
if entry_infos.get(CONF_HEATER_4): if config_entry.get(CONF_HEATER_4):
lst_switches.append(entry_infos.get(CONF_HEATER_4)) lst_switches.append(config_entry.get(CONF_HEATER_4))
delta_cycle = self._cycle_min * 60 / len(lst_switches) delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches): for idx, switch in enumerate(lst_switches):
@@ -104,7 +104,7 @@ class ThermostatOverSwitch(BaseThermostat):
) )
) )
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
@overrides @overrides

View File

@@ -3,7 +3,10 @@
import logging import logging
from datetime import timedelta from datetime import timedelta
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
@@ -16,39 +19,53 @@ from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat): class ThermostatOverValve(BaseThermostat):
"""Representation of a class for a Versatile Thermostat over a Valve""" """Representation of a class for a Versatile Thermostat over a Valve"""
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset( _entity_component_unrecorded_attributes = (
{ BaseThermostat._entity_component_unrecorded_attributes.union(
"is_over_valve", "underlying_valve_0", "underlying_valve_1", frozenset(
"underlying_valve_2", "underlying_valve_3", "on_time_sec", "off_time_sec", {
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext" "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",
}
)
)
)
# Useless for now # Useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos) # super().__init__(hass, unique_id, name, config_entry)
@property @property
def is_over_valve(self) -> bool: def is_over_valve(self) -> bool:
""" True if the Thermostat is over_valve""" """True if the Thermostat is over_valve"""
return True return True
@property @property
def valve_open_percent(self) -> int: def valve_open_percent(self) -> int:
""" Gives the percentage of valve needed""" """Gives the percentage of valve needed"""
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
return 0 return 0
else: else:
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100) return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, config_entry):
""" Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm( self._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
self._tpi_coef_int, self._tpi_coef_int,
@@ -57,21 +74,17 @@ class ThermostatOverValve(BaseThermostat):
self._minimal_activation_delay, self._minimal_activation_delay,
) )
lst_valves = [entry_infos.get(CONF_VALVE)] lst_valves = [config_entry.get(CONF_VALVE)]
if entry_infos.get(CONF_VALVE_2): if config_entry.get(CONF_VALVE_2):
lst_valves.append(entry_infos.get(CONF_VALVE_2)) lst_valves.append(config_entry.get(CONF_VALVE_2))
if entry_infos.get(CONF_VALVE_3): if config_entry.get(CONF_VALVE_3):
lst_valves.append(entry_infos.get(CONF_VALVE_3)) lst_valves.append(config_entry.get(CONF_VALVE_3))
if entry_infos.get(CONF_VALVE_4): if config_entry.get(CONF_VALVE_4):
lst_valves.append(entry_infos.get(CONF_VALVE_4)) lst_valves.append(config_entry.get(CONF_VALVE_4))
for _, valve in enumerate(lst_valves): for _, valve in enumerate(lst_valves):
self._underlyings.append( self._underlyings.append(
UnderlyingValve( UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve)
hass=self._hass,
thermostat=self,
valve_entity_id=valve
)
) )
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
@@ -89,7 +102,7 @@ class ThermostatOverValve(BaseThermostat):
async_track_state_change_event( async_track_state_change_event(
self.hass, [valve.entity_id], self._async_valve_changed self.hass, [valve.entity_id], self._async_valve_changed
) )
) )
# Start the control_heating # Start the control_heating
# starts a cycle # starts a cycle
@@ -107,29 +120,34 @@ class ThermostatOverValve(BaseThermostat):
This method just log the change. It changes nothing to avoid loops. This method just log the change. It changes nothing to avoid loops.
""" """
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state) _LOGGER.debug(
"%s - _async_valve_changed new_state is %s", self, new_state.state
)
@overrides @overrides
def update_custom_attributes(self): def update_custom_attributes(self):
""" Custom attributes """ """Custom attributes"""
super().update_custom_attributes() super().update_custom_attributes()
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent self._attr_extra_state_attributes[
"valve_open_percent"
] = self.valve_open_percent
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
self._attr_extra_state_attributes["underlying_valve_0"] = ( self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
self._underlyings[0].entity_id) 0
].entity_id
self._attr_extra_state_attributes["underlying_valve_1"] = ( self._attr_extra_state_attributes["underlying_valve_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
) )
self._attr_extra_state_attributes["underlying_valve_2"] = ( self._attr_extra_state_attributes["underlying_valve_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
) )
self._attr_extra_state_attributes["underlying_valve_3"] = ( self._attr_extra_state_attributes["underlying_valve_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
) )
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"on_percent" "on_percent"
] = self._prop_algorithm.on_percent ] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"on_time_sec" "on_time_sec"
] = self._prop_algorithm.on_time_sec ] = self._prop_algorithm.on_time_sec
@@ -162,9 +180,7 @@ class ThermostatOverValve(BaseThermostat):
) )
for under in self._underlyings: for under in self._underlyings:
under.set_valve_open_percent( under.set_valve_open_percent()
self._prop_algorithm.on_percent
)
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
@@ -185,4 +201,4 @@ class ThermostatOverValve(BaseThermostat):
self, self,
added_energy, added_energy,
self._total_energy, self._total_energy,
) )

View File

@@ -24,6 +24,7 @@
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximal temperature allowed",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')",
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
@@ -31,6 +32,7 @@
"use_main_central_config": "Use central main configuration" "use_main_central_config": "Use central main configuration"
}, },
"data_description": { "data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
@@ -254,6 +256,7 @@
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximal temperature allowed",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')",
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
@@ -261,6 +264,7 @@
"use_main_central_config": "Use central main configuration" "use_main_central_config": "Use central main configuration"
}, },
"data_description": { "data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
@@ -413,6 +417,7 @@
"eco_away_temp": "Eco away preset", "eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset", "comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset", "boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode", "eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort away preset in AC mode", "comfort_ac_away_temp": "Comfort away preset in AC mode",
"boost_ac_away_temp": "Boost away preset in AC mode", "boost_ac_away_temp": "Boost away preset in AC mode",

View File

@@ -24,6 +24,7 @@
"temp_min": "Température minimale permise", "temp_min": "Température minimale permise",
"temp_max": "Température maximale permise", "temp_max": "Température maximale permise",
"device_power": "Puissance de l'équipement", "device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par le mode central ('central_mode`)",
"use_window_feature": "Avec détection des ouvertures", "use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement", "use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance", "use_power_feature": "Avec gestion de la puissance",
@@ -31,6 +32,7 @@
"use_main_central_config": "Utiliser la configuration centrale principale" "use_main_central_config": "Utiliser la configuration centrale principale"
}, },
"data_description": { "data_description": {
"use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale",
"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", "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",
"use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale" "use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale"
} }
@@ -254,6 +256,7 @@
"temp_min": "Température minimale permise", "temp_min": "Température minimale permise",
"temp_max": "Température maximale permise", "temp_max": "Température maximale permise",
"device_power": "Puissance de l'équipement", "device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par le mode central ('central_mode`)",
"use_window_feature": "Avec détection des ouvertures", "use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement", "use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance", "use_power_feature": "Avec gestion de la puissance",
@@ -261,6 +264,7 @@
"use_main_central_config": "Utiliser la configuration centrale" "use_main_central_config": "Utiliser la configuration centrale"
}, },
"data_description": { "data_description": {
"use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale",
"use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique", "use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique",
"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" "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"
} }

View File

@@ -6,6 +6,7 @@ from typing import Any
from enum import StrEnum from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.core import State
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
@@ -111,18 +112,18 @@ class UnderlyingEntity:
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test # This should be the correct way to handle turn_off and turn_on but this breaks the unit test
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression # will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self): async def turn_off(self):
""" Turn off the underlying equipement. """Turn off the underlying equipement.
Need to be overriden""" Need to be overriden"""
return NotImplementedError return NotImplementedError
async def turn_on(self): async def turn_on(self):
""" Turn off the underlying equipement. """Turn off the underlying equipement.
Need to be overriden""" Need to be overriden"""
return NotImplementedError return NotImplementedError
@property @property
def is_inversed(self): def is_inversed(self):
""" Tells if the switch command should be inversed""" """Tells if the switch command should be inversed"""
return False return False
def remove_entity(self): def remove_entity(self):
@@ -140,8 +141,9 @@ class UnderlyingEntity:
await self.set_hvac_mode(hvac_mode) await self.set_hvac_mode(hvac_mode)
elif hvac_mode != HVACMode.OFF and not self.is_device_active: elif hvac_mode != HVACMode.OFF and not self.is_device_active:
_LOGGER.warning( _LOGGER.warning(
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s", "%s - The hvac mode is %s, but the underlying device is not ON. Turning on device %s if needed",
self, self,
hvac_mode,
self._entity_id, self._entity_id,
) )
await self.set_hvac_mode(hvac_mode) await self.set_hvac_mode(hvac_mode)
@@ -164,7 +166,11 @@ class UnderlyingEntity:
"""Starting cycle for switch""" """Starting cycle for switch"""
def _cancel_cycle(self): def _cancel_cycle(self):
""" Stops an eventual cycle """ """Stops an eventual cycle"""
def cap_sent_value(self, value) -> float:
"""capping of the value send to the underlying eqt"""
return value
class UnderlyingSwitch(UnderlyingEntity): class UnderlyingSwitch(UnderlyingEntity):
@@ -205,7 +211,7 @@ class UnderlyingSwitch(UnderlyingEntity):
@overrides @overrides
@property @property
def is_inversed(self): def is_inversed(self):
""" Tells if the switch command should be inversed""" """Tells if the switch command should be inversed"""
return self._thermostat.is_inversed return self._thermostat.is_inversed
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
@@ -227,14 +233,16 @@ class UnderlyingSwitch(UnderlyingEntity):
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
real_state = self._hass.states.is_state(self._entity_id, STATE_ON) real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state) return (self.is_inversed and not real_state) or (
not self.is_inversed and real_state
)
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
domain = self._entity_id.split('.')[0] domain = self._entity_id.split(".")[0]
# This may fails if called after shutdown # This may fails if called after shutdown
try: try:
data = {ATTR_ENTITY_ID: self._entity_id} data = {ATTR_ENTITY_ID: self._entity_id}
@@ -250,7 +258,7 @@ class UnderlyingSwitch(UnderlyingEntity):
"""Turn heater toggleable device on.""" """Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
domain = self._entity_id.split('.')[0] domain = self._entity_id.split(".")[0]
try: try:
data = {ATTR_ENTITY_ID: self._entity_id} data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call( await self._hass.services.async_call(
@@ -261,7 +269,6 @@ class UnderlyingSwitch(UnderlyingEntity):
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
@overrides @overrides
async def start_cycle( async def start_cycle(
self, self,
@@ -348,7 +355,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if await self._thermostat.check_overpowering(): if await self._thermostat.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self) _LOGGER.debug("%s - End of cycle (3)", self)
return return
# Security mode could have change the on_time percent # safety mode could have change the on_time percent
await self._thermostat.check_security() await self._thermostat.check_security()
time = self._on_time_sec time = self._on_time_sec
@@ -490,10 +497,14 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
if self.is_initialized: if self.is_initialized:
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [ return (
HVACAction.IDLE, self._underlying_climate.hvac_mode != HVACMode.OFF
HVACAction.OFF, and self._underlying_climate.hvac_action
] not in [
HVACAction.IDLE,
HVACAction.OFF,
]
)
else: else:
return None return None
@@ -550,7 +561,7 @@ class UnderlyingClimate(UnderlyingEntity):
return return
data = { data = {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
"temperature": temperature, "temperature": self.cap_sent_value(temperature),
"target_temp_high": max_temp, "target_temp_high": max_temp,
"target_temp_low": min_temp, "target_temp_low": min_temp,
} }
@@ -664,6 +675,40 @@ class UnderlyingClimate(UnderlyingEntity):
return None return None
return self._underlying_climate.turn_aux_heat_off() return self._underlying_climate.turn_aux_heat_off()
@overrides
def cap_sent_value(self, value) -> float:
"""Try to adapt the target temp value to the min_temp / max_temp found
in the underlying entity (if any)"""
if not self.is_initialized:
return value
# Gets the min_temp and max_temp
if (
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
new_value = max(min_val, min(value, max_val))
else:
_LOGGER.debug("%s - no min and max attributes on underlying", self)
new_value = value
if new_value != value:
_LOGGER.info(
"%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
self,
new_value,
value,
min_val,
max_val,
)
return new_value
class UnderlyingValve(UnderlyingEntity): class UnderlyingValve(UnderlyingEntity):
"""Represent a underlying switch""" """Represent a underlying switch"""
@@ -672,10 +717,7 @@ class UnderlyingValve(UnderlyingEntity):
_percent_open: int _percent_open: int
def __init__( def __init__(
self, self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
hass: HomeAssistant,
thermostat: Any,
valve_entity_id: str
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -689,13 +731,14 @@ class UnderlyingValve(UnderlyingEntity):
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
self._hvac_mode = None self._hvac_mode = None
self._percent_open = self._thermostat.valve_open_percent self._percent_open = self._thermostat.valve_open_percent
self._valve_entity_id = valve_entity_id
async def send_percent_open(self): async def send_percent_open(self):
""" Send the percent open to the underlying valve """ """Send the percent open to the underlying valve"""
# This may fails if called after shutdown # This may fails if called after shutdown
try: try:
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open } data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
domain = self._entity_id.split('.')[0] domain = self._entity_id.split(".")[0]
await self._hass.services.async_call( await self._hass.services.async_call(
domain, domain,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@@ -734,7 +777,7 @@ class UnderlyingValve(UnderlyingEntity):
# To test if real device is open but this is causing some side effect # To test if real device is open but this is causing some side effect
# because the activation can be deferred - # because the activation can be deferred -
# or float(self._hass.states.get(self._entity_id).state) > 0 # or float(self._hass.states.get(self._entity_id).state) > 0
except Exception: # pylint: disable=broad-exception-caught except Exception: # pylint: disable=broad-exception-caught
return False return False
@overrides @overrides
@@ -748,11 +791,43 @@ class UnderlyingValve(UnderlyingEntity):
): ):
"""We use this function to change the on_percent""" """We use this function to change the on_percent"""
if force: if force:
self._percent_open = self.cap_sent_value(self._percent_open)
await self.send_percent_open() await self.send_percent_open()
def set_valve_open_percent(self, percent): @overrides
""" Update the valve open percent """ def cap_sent_value(self, value) -> float:
caped_val = self._thermostat.valve_open_percent """Try to adapt the open_percent value to the min / max found
in the underlying entity (if any)"""
# Gets the last number state
valve_state: State = self._hass.states.get(self._valve_entity_id)
if valve_state is None:
return value
if "min" in valve_state.attributes and "max" in valve_state.attributes:
min_val = valve_state.attributes["min"]
max_val = valve_state.attributes["max"]
new_value = round(max(min_val, min(value, max_val)))
else:
_LOGGER.debug("%s - no min and max attributes on underlying", self)
new_value = value
if new_value != value:
_LOGGER.info(
"%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
self,
new_value,
value,
min_val,
max_val,
)
return new_value
def set_valve_open_percent(self):
"""Update the valve open percent"""
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
if self._percent_open == caped_val: if self._percent_open == caped_val:
# No changes # No changes
return return
@@ -760,7 +835,9 @@ class UnderlyingValve(UnderlyingEntity):
self._percent_open = caped_val self._percent_open = caped_val
# Send the new command to valve via a service call # Send the new command to valve via a service call
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open) _LOGGER.info(
"%s - Setting valve ouverture percent to %s", self, self._percent_open
)
# Send the change to the valve, in background # Send the change to the valve, in background
self._hass.create_task(self.send_percent_open()) self._hass.create_task(self.send_percent_open())

View File

@@ -19,26 +19,26 @@ _LOGGER = logging.getLogger(__name__)
class VersatileThermostatAPI(dict): class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI""" """The VersatileThermostatAPI"""
_hass: HomeAssistant _hass: HomeAssistant = None
# _entries: Dict(str, ConfigEntry)
@classmethod @classmethod
def get_vtherm_api(cls, hass=None): def get_vtherm_api(cls, hass=None):
"""Get the eventual VTherm API class instance""" """Get the eventual VTherm API class instance or
instantiate it if it doesn't exists"""
if hass is not None: if hass is not None:
VersatileThermostatAPI._hass = hass VersatileThermostatAPI._hass = hass
else:
if VersatileThermostatAPI._hass is None: if VersatileThermostatAPI._hass is None:
return None return None
domain = VersatileThermostatAPI._hass.data.get(DOMAIN) domain = VersatileThermostatAPI._hass.data.get(DOMAIN)
if not domain: if not domain:
hass.data.setdefault(DOMAIN, {}) VersatileThermostatAPI._hass.data.setdefault(DOMAIN, {})
ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME) ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME)
if ret is None: if ret is None:
ret = VersatileThermostatAPI() ret = VersatileThermostatAPI()
hass.data[DOMAIN][VTHERM_API_NAME] = ret VersatileThermostatAPI._hass.data[DOMAIN][VTHERM_API_NAME] = ret
return ret return ret
def __init__(self) -> None: def __init__(self) -> None:
@@ -46,7 +46,6 @@ class VersatileThermostatAPI(dict):
super().__init__() super().__init__()
self._expert_params = None self._expert_params = None
self._short_ema_params = None self._short_ema_params = None
self._central_config = None
def find_central_configuration(self): def find_central_configuration(self):
"""Search for a central configuration""" """Search for a central configuration"""
@@ -57,21 +56,19 @@ class VersatileThermostatAPI(dict):
config_entry.data.get(CONF_THERMOSTAT_TYPE) config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG == CONF_THERMOSTAT_CENTRAL_CONFIG
): ):
self._central_config = config_entry central_config = config_entry
return self._central_config return central_config
return None return None
def add_entry(self, entry: ConfigEntry): def add_entry(self, entry: ConfigEntry):
"""Add a new entry""" """Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id) _LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data # Add the entry in hass.data
VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry): def remove_entry(self, entry: ConfigEntry):
"""Remove an entry""" """Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id) _LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id) VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API # If not more entries are preset, remove the API
if len(self) == 0: if len(self) == 0:

BIN
images/central_mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 45 KiB

BIN
images/config-main0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
images/plotly-curves.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

7
pyrightconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"include": [
"custom_components/versatile_thermostat/**",
"homeassistant/**"
],
"reportShadowedImports": false
}

View File

@@ -185,6 +185,7 @@ class MockClimate(ClimateEntity):
hvac_mode: HVACMode = HVACMode.OFF, hvac_mode: HVACMode = HVACMode.OFF,
hvac_action: HVACAction = HVACAction.OFF, hvac_action: HVACAction = HVACAction.OFF,
fan_modes: list[str] = None, fan_modes: list[str] = None,
hvac_modes: list[str] = None,
) -> None: ) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
@@ -200,7 +201,11 @@ class MockClimate(ClimateEntity):
HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
) )
self._attr_hvac_mode = hvac_mode self._attr_hvac_mode = hvac_mode
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] self._attr_hvac_modes = (
hvac_modes
if hvac_modes is not None
else [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
)
self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_target_temperature = 20 self._attr_target_temperature = 20
self._attr_current_temperature = 15 self._attr_current_temperature = 15
@@ -336,6 +341,14 @@ class MagicMockClimate(MagicMock):
def supported_features(self): # pylint: disable=missing-function-docstring def supported_features(self): # pylint: disable=missing-function-docstring
return ClimateEntityFeature.TARGET_TEMPERATURE return ClimateEntityFeature.TARGET_TEMPERATURE
@property
def min_temp(self): # pylint: disable=missing-function-docstring
return 15
@property
def max_temp(self): # pylint: disable=missing-function-docstring
return 19
async def create_thermostat( async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str hass: HomeAssistant, entry: MockConfigEntry, entity_id: str

View File

@@ -50,7 +50,8 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1, CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: True
# Keep default values which are False # Keep default values which are False
} }
@@ -139,6 +140,11 @@ MOCK_PRESETS_AC_CONFIG = {
MOCK_WINDOW_CONFIG = { MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
# Not used normally only for tests to avoid rewrite all tests
CONF_WINDOW_DELAY: 10,
}
MOCK_WINDOW_DELAY_CONFIG = {
CONF_WINDOW_DELAY: 10, CONF_WINDOW_DELAY: 10,
} }

View File

@@ -6,6 +6,10 @@ from datetime import datetime, timedelta
import logging import logging
from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
)
from .commons import * from .commons import *
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -354,7 +358,7 @@ async def test_bug_82(
skip_turn_on_off_heater, skip_turn_on_off_heater,
skip_send_event, skip_send_event,
): ):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode""" """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 tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
@@ -423,7 +427,7 @@ async def test_bug_82(
assert mock_find_climate.mock_calls[0] == call() assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()]) mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode # Force safety mode
assert entity._last_ext_temperature_measure is not None assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None assert entity._last_temperature_measure is not None
assert ( assert (
@@ -568,3 +572,141 @@ async def test_bug_101(
) )
assert entity.target_temperature == 12.75 assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE assert entity.preset_mode is PRESET_NONE
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_272(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it not possible to set the target temperature under the min_temp setting"""
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
)
# Min_temp is 15 and max_temp is 19
fake_underlying_climate = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call:
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
assert entity.target_temperature == entity.min_temp
assert entity.is_regulated is True
assert mock_service_call.call_count == 0
# Set the hvac_mode to HEAT
await entity.async_set_hvac_mode(HVACMode.HEAT)
# In the accepted interval
await entity.async_set_temperature(temperature=17.5)
assert mock_service_call.call_count == 2
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_HVAC_MODE,
{"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
),
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"temperature": 17.5,
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Set room temperature to something very cold
event_timestamp = now + timedelta(minutes=1)
await send_temperature_change_event(entity, 13, event_timestamp)
await send_ext_temperature_change_event(entity, 9, event_timestamp)
# Not in the accepted interval (15-19)
await entity.async_set_temperature(temperature=10)
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"temperature": 15, # the minimum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Set room temperature to something very cold
event_timestamp = now + timedelta(minutes=1)
await send_temperature_change_event(entity, 13, event_timestamp)
await send_ext_temperature_change_event(entity, 9, event_timestamp)
# In the accepted interval
await entity.async_set_temperature(temperature=20.8)
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"temperature": 19, # the maximum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)

View File

@@ -31,6 +31,8 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state): async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
"""Tests the clean_central_config_doubon of base_thermostat""" """Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry( central_config_entry = MockConfigEntry(
@@ -95,6 +97,8 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
assert central_configuration is not None assert central_configuration is not None
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_minimal_over_switch_wo_central_config( async def test_minimal_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
): ):
@@ -169,6 +173,8 @@ async def test_minimal_over_switch_wo_central_config(
assert entity.is_inversed assert entity.is_inversed
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_wo_central_config( async def test_full_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
): ):
@@ -281,6 +287,8 @@ async def test_full_over_switch_wo_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_with_central_config( async def test_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -388,6 +396,8 @@ async def test_full_over_switch_with_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_with_central_config_but_no_central_config( async def test_over_switch_with_central_config_but_no_central_config(
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
): ):

1040
tests/test_central_mode.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -363,6 +363,7 @@ async def test_user_config_flow_window_auto_ok(
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_WINDOW_DELAY: 30, # the default value is added CONF_WINDOW_DELAY: 30, # the default value is added
CONF_USE_CENTRAL_MODE: True, # True is the defaulf value
} | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | { } | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: False, CONF_USE_TPI_CENTRAL_CONFIG: False,
@@ -472,15 +473,17 @@ async def test_user_config_flow_window_auto_ko(
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=MOCK_WINDOW_AUTO_CONFIG, user_input=MOCK_WINDOW_DELAY_CONFIG,
) )
# Since issue #280 we cannot have the error because we only display the
# MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# We should stay on window with an error # We should stay on window with an error
assert result["errors"] == { assert result["errors"] == {}
"window_sensor_entity_id": "window_open_detection_method" # "window_sensor_entity_id": "window_open_detection_method"
} # }
assert result["step_id"] == "window" assert result["step_id"] == "advanced"
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -508,6 +511,7 @@ async def test_user_config_flow_over_4_switches(
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: False,
} }
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name

View File

@@ -288,7 +288,7 @@ async def test_security_feature_back_on_percent(
assert entity.security_state is False assert entity.security_state is False
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# 3. Set security mode with a preset change # 3. Set safety mode with a preset change
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
@@ -400,7 +400,7 @@ async def test_security_over_climate(
skip_turn_on_off_heater, skip_turn_on_off_heater,
skip_send_event, skip_send_event,
): ):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode""" """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 tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
@@ -471,7 +471,7 @@ async def test_security_over_climate(
assert mock_find_climate.mock_calls[0] == call() assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()]) mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode # Force safety mode
assert entity._last_ext_temperature_measure is not None assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None assert entity._last_temperature_measure is not None
assert ( assert (
@@ -505,7 +505,7 @@ async def test_security_over_climate(
event_timestamp = now - timedelta(minutes=6) event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode # Should stay False because a climate is never in safety mode
assert entity.security_state is False assert entity.security_state is False
assert entity.preset_mode == "none" assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none" assert entity._saved_preset_mode == "none"

View File

@@ -129,7 +129,7 @@ async def test_over_switch_ac_full_start(
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == 16 # eco_ac_away assert entity.target_temperature == 27 # eco_ac_away (no change)
# Close a window # Close a window
with patch("homeassistant.helpers.condition.state", return_value=True): with patch("homeassistant.helpers.condition.state", return_value=True):

View File

@@ -4,7 +4,7 @@
from unittest.mock import patch, call from unittest.mock import patch, call
from datetime import datetime, timedelta from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, State
from homeassistant.components.climate import HVACAction, HVACMode from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@@ -214,20 +214,60 @@ async def test_over_valve_full_start(
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# Change internal temperature # Change internal temperature
expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
)
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch( ) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=0 "homeassistant.core.StateMachine.get", return_value=expected_state
): ):
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 20, datetime.now()) await send_temperature_change_event(entity, 20, datetime.now())
assert entity.valve_open_percent == 0 assert entity.valve_open_percent == 0
assert entity.is_device_active is False assert entity.is_device_active is True # Should be 0 but in fact 10 is send
assert entity.hvac_action == HVACAction.IDLE assert (
entity.hvac_action == HVACAction.HEATING
) # Should be IDLE but heating due to 10
assert mock_service_call.call_count == 1
# The VTherm valve is 0, but the underlying have received 10 which is the min
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 10},
)
]
)
await send_temperature_change_event(entity, 17, datetime.now()) await send_temperature_change_event(entity, 17, datetime.now())
assert mock_service_call.call_count == 2
# The VTherm valve is 0, but the underlying have received 10 which is the min
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_valve",
"value": 10,
}, # the min allowed value
),
call.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_valve",
"value": 50,
}, # the max allowed value
),
]
)
# switch to Eco # switch to Eco
await entity.async_set_preset_mode(PRESET_ECO) await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO assert entity.preset_mode is PRESET_ECO
@@ -243,6 +283,18 @@ async def test_over_valve_full_start(
assert entity.is_device_active is True assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING 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)
expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99}
)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=expected_state
):
# Open a window # Open a window
with patch("homeassistant.helpers.condition.state", return_value=True): with patch("homeassistant.helpers.condition.state", return_value=True):
event_timestamp = now - timedelta(minutes=1) event_timestamp = now - timedelta(minutes=1)

View File

@@ -1,4 +1,4 @@
# pylint: disable=unused-argument, line-too-long, protected-access # pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines
""" Test the Window management """ """ Test the Window management """
import asyncio import asyncio
import logging import logging