Compare commits

..

4 Commits

Author SHA1 Message Date
Jean-Marc Collin
229cb19a17 Algo fixes 2024-01-27 17:00:00 +00:00
Jean-Marc Collin
2fe0e57231 Change algo using underlying internal temp 2024-01-27 11:46:01 +00:00
Jean-Marc Collin
de47a3ffe1 With all features + testu ok 2024-01-26 19:49:04 +00:00
Jean-Marc Collin
85dcac9530 Add config option 2024-01-26 18:07:54 +00:00
64 changed files with 1400 additions and 4459 deletions

View File

@@ -1,2 +0,0 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.12
RUN apt update && apt install -y ffmpeg

View File

@@ -1,11 +1,14 @@
default_config: default_config:
# ffmeg
ffmpeg:
logger: logger:
default: warning default: info
logs: logs:
# custom_components.versatile_thermostat: debug custom_components.versatile_thermostat: debug
# custom_components.versatile_thermostat.underlyings: debug custom_components.versatile_thermostat.underlyings: debug
# custom_components.versatile_thermostat.climate: debug custom_components.versatile_thermostat.climate: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) # If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy: debugpy:

View File

@@ -1,9 +1,7 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details. // See https://aka.ms/vscode-remote/devcontainer.json for format details.
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest", // "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
{ {
"build": { "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
"dockerfile": "Dockerfile"
},
"name": "Versatile Thermostat integration", "name": "Versatile Thermostat integration",
"appPort": [ "appPort": [
"8123:8123" "8123:8123"
@@ -11,34 +9,29 @@
// "postCreateCommand": "container install", // "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup", "postCreateCommand": "./container dev-setup",
"mounts": [ "mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached", "source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card // uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached" "source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
], ],
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
"ms-python.python", "ms-python.python",
"ms-python.pylint",
// Doesn't work (crash). Default in python is to use Jedi see Settings / Python / Default Language
// "ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"github.vscode-pull-request-github", "github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters", "ryanluker.vscode-coverage-gutters",
"ms-python.black-formatter",
"ms-python.pylint",
"ferrierbenjamin.fold-unfold-all-icone", "ferrierbenjamin.fold-unfold-all-icone",
"ms-python.isort",
"LittleFoxTeam.vscode-python-test-adapter", "LittleFoxTeam.vscode-python-test-adapter",
"donjayamanne.githistory", "donjayamanne.githistory",
"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",
"github.vscode-github-actions", "github.vscode-github-actions"
"azuretools.vscode-docker"
], ],
"settings": { "settings": {
"files.eol": "\n", "files.eol": "\n",
@@ -59,10 +52,10 @@
"editor.formatOnPaste": false, "editor.formatOnPaste": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnType": true, "editor.formatOnType": true,
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true,
// "python.experiments.optOutFrom": ["pythonTestAdapter"], "python.experiments.optOutFrom": ["pythonTestAdapter"],
// "python.analysis.logLevel": "Trace" "python.analysis.logLevel": "Trace"
} }
} }
} }
} }

View File

@@ -17,7 +17,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.12 python-version: 3.11
- name: Install dependencies - name: Install dependencies
run: | run: |

30
.vscode/launch.json vendored
View File

@@ -1,14 +1,18 @@
{ {
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Home Assistant (debug)", "name": "Home Assistant (debug)",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"module": "homeassistant", "module": "homeassistant",
"justMyCode": false, "justMyCode": false,
"args": ["--debug", "-c", "config"] "args": [
} "--debug",
] "-c",
} "config"
]
}
]
}

12
.vscode/settings.json vendored
View File

@@ -1,19 +1,21 @@
{ {
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true, "editor.formatOnSave": true
"editor.formatOnSaveMode": "modifications"
}, },
"pylint.lintOnChange": false, "pylint.lintOnChange": false,
"files.associations": { "files.associations": {
"*.yaml": "home-assistant" "*.yaml": "home-assistant"
}, },
"python.testing.pytestArgs": [], "python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"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.12/site-packages/homeassistant" "/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
] ],
"python.formatting.provider": "none"
} }

View File

@@ -4,15 +4,11 @@
[![hacs][hacs_badge]][hacs] [![hacs][hacs_badge]][hacs]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
![Tip](images/icon.png) ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](images/tips.png) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-). > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
- [Changements dans la version 6.0](#changements-dans-la-version-60) - [Changements majeurs dans la version 5.0](#changements-majeurs-dans-la-version-50)
- [Entités de température pour les pre-réglages](#entités-de-température-pour-les-pre-réglages)
- [Dans le cas d'une configuration centrale](#dans-le-cas-dune-configuration-centrale)
- [Refonte du menu de configuration](#refonte-du-menu-de-configuration)
- [Les options de menu 'Configuration incomplète' et 'Finaliser'](#les-options-de-menu-configuration-incomplète-et-finaliser)
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee) - [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser) - [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Incompatibilités](#incompatibilités) - [Incompatibilités](#incompatibilités)
@@ -23,23 +19,21 @@
- [Configuration](#configuration) - [Configuration](#configuration)
- [Création d'un nouveau Versatile Thermostat](#création-dun-nouveau-versatile-thermostat) - [Création d'un nouveau Versatile Thermostat](#création-dun-nouveau-versatile-thermostat)
- [Choix des attributs de base](#choix-des-attributs-de-base) - [Choix des attributs de base](#choix-des-attributs-de-base)
- [Sélectionnez des entités pilotées (sous-jacents)](#sélectionnez-des-entités-pilotées-sous-jacents) - [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
- [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch) - [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
- [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate) - [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate)
- [L'auto-régulation](#lauto-régulation) - [L'auto-régulation](#lauto-régulation)
- [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert) - [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert)
- [Compensation de la température interne](#compensation-de-la-température-interne)
- [Synthèse de l'algorithme d'auto-régulation](#synthèse-de-lalgorithme-dauto-régulation)
- [Le mode auto-fan](#le-mode-auto-fan) - [Le mode auto-fan](#le-mode-auto-fan)
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve) - [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi) - [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
- [Configurer les températures préréglées](#configurer-les-températures-préréglées) - [Configurer la température préréglée](#configurer-la-température-préréglée)
- [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats) - [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats)
- [Le mode capteur](#le-mode-capteur) - [Le mode capteur](#le-mode-capteur)
- [Le mode auto](#le-mode-auto) - [Le mode auto](#le-mode-auto)
- [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement) - [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement)
- [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'absence)](#configurer-la-présence-ou-labsence) - [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é) - [Le contrôle centralisé](#le-contrôle-centralisé)
- [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale) - [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale)
@@ -85,25 +79,16 @@
- [Comment être averti lorsque cela se produit ?](#comment-être-averti-lorsque-cela-se-produit-) - [Comment être averti lorsque cela se produit ?](#comment-être-averti-lorsque-cela-se-produit-)
- [Comment réparer ?](#comment-réparer-) - [Comment réparer ?](#comment-réparer-)
- [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence) - [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence)
- [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat)
Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités. Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités.
> ![Nouveau](images/new-icon.png) _*Historique des dernières versions*_ > ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **Release 6.0** : > * **Release 5.4** : Ajout du pas de température [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311). Ajout de seuils de régulation pour les `over_valve` pour éviter de trop vider la batterie des TRV [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338)
> - Ajout d'entités du domaine Number permettant de configurer les températures des presets [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> - Refonte complète du menu de configuration pour supprimer les températures et utililsation d'un menu au lieu d'un tunnel de configuration [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> * **Release 5.4** :
> - Ajout du pas de température [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311),
> - ajout de seuils de régulation pour les `over_valve` pour éviter de trop vider la batterie des TRV [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
> - ajout d'une option permettant d'utiliser la température interne d'un TRV pour forcer l' auto-régulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
> - ajout d'une fonction de keep-alive pour les VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343) > * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158). > * **Release 5.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.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).
<details> <details>
<summary>Autres versions</summary> <summary>Autres versions</summary>
@@ -125,76 +110,8 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier. > * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
</details> </details>
# Changements dans la version 6.0 # Changements majeurs dans la version 5.0
![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true)
## Entités de température pour les pre-réglages
Les températures des presets sont maintenant directement acessibles sous la forme d'entités reliés au VTherm.
Exemple :
![Entités température](images/temp-entities-1.png)
Les entités Boost, Confort, Eco et Hors-gel permettent de régler directement les températures de ces présets sans avoir à reconfigurer le VTHerm dans les écrans de configuration.
Ces modifications sont persistentent à un redémarrage et sont prises en compte immédiatement par le VTherm.
En fonction des fonctions activées, la liste des températures peut être plus ou moins complète :
1. Si la gestion de présence est activée, les presets en cas d'absence sont créés. Ils sont suffixés par 'abs' pour absence,
2. Si la gestion de la climatisation (Mode AC) est activé, les presets en mode clim sont créés. Ils sont suffixés par 'clim' pour climatisation. Seul le preset Hors gel n'a pas d'équivalent en mode clim,
3. Les différentes combinaison absent et clim peuvent être créés en fonction de la configuration du VTherm
Si un VTherm utilise les preset de la configuration centrale, ces entités ne sont pas créées, car les températures des presets sont gérés par la configuration centrale.
### Dans le cas d'une configuration centrale
Si vous avez configuré une configuration centrale, celle-ci possède aussi ses propres presets qui répondent au même règles qu'énoncées ci-dessus.
Exemple d'une configuration centrale avec gestion de présence et mode AC (climatisation) :
![Entités température](images/temp-entities-2.png)
Dans le cas d'un changement d'une température de la configuration centrale, tous les VTherm qui utilisent ce preset sont immédiatement mis à jour.
## Refonte du menu de configuration
Le menu de configuration a été totalement revu. Il s'adapte dynamiquement aux choix de l'utilisateur et permet d'accéder directement aux réglages de la fonction voulue sans avoir à dérouler tous le tunnel de configuration.
Pour créer un nouveau VTherm, il faudra d'abord choisir le type de VTherm :
![Choix VTherm](images/config-main0.png)
Puis, vous accédez maintenant au menu de configuration suivant :
![VTherm menu](images/config-menu.png)
Chaque partie à configurer est accessible directement, sans avoir à dérouler tout le tunnel de configuration comme précédemment.
Vous noterez l'option de menu nommée `Fonctions` qui permet de choisir quelles fonctions vont être implémentées pour ce VTherm :
![VTherm fonctions](images/config-features.png)
En fonction de vos choix, le menu principal s'adaptera pour ajouter les options nécessaires.
Exemple de menu avec toutes les fonctions cochées :
![VTherm menu](images/config-menu-all-options.png)
Vous pouvez constater que les options 'Détection des ouvertures', 'Détection de mouvement', 'Gestion de la puissance' et 'Gestion de présence' ont été ajoutées. Vous pouvez alors les configurer.
### Les options de menu 'Configuration incomplète' et 'Finaliser'
La dernière option du menu est spéciale. Elle permet de valider la création du VTherm lorsque toutes les fonctions ont été correctement configurées.
Si l'une options n'est pas correctement configurée, la dernière option est la suivante :
![Configuration incomplète](images/config-not-complete.png)
Sa sélection ne fait rien mais vous empêche de finaliser la création (resp. la modification) du VTherm.
**Vous devez alors chercher dans les options laquelle manque**.
Une fois que toute la configuration est valide, la dernière option se transforme en :
![Configuration complète](images/config-complete.png)
Cliquez sur cette option pour créér (resp. modifier) le VTherm :
![Configuration terminée](images/config-terminate.png)
<details>
<summary>Changements dans la version 5.0</summary>
Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez : Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez :
1. Créer un VTherm de type "Configuration Centrale", 1. Créer un VTherm de type "Configuration Centrale",
@@ -208,10 +125,10 @@ Lors d'un changement sur la configuration centrale, tous les VTherms seront rech
En conséquence toute la phase de paramètrage d'un VTherm a été profondemment modifiée pour pouvoir utiliser la configuration centrale ou surcharger les valeurs de la configuration centrale par des valeurs propre au VTherm en cours de configuration. En conséquence toute la phase de paramètrage d'un VTherm a été profondemment modifiée pour pouvoir utiliser la configuration centrale ou surcharger les valeurs de la configuration centrale par des valeurs propre au VTherm en cours de configuration.
</details> **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 @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R. pour les bières. Ca fait très plaisir et ça m'encourage à continuer ! Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco 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
@@ -234,7 +151,7 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme, 2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature 3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement. 4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel. 5. les TRV de type Aqara SRTS-A01 qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
# Pourquoi une nouvelle implémentation du thermostat ? # Pourquoi une nouvelle implémentation du thermostat ?
@@ -277,7 +194,7 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
-- VTherm = Versatile Thermostat dans la suite de ce document -- -- VTherm = Versatile Thermostat dans la suite de ce document --
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](/images/tips.png?raw=true) _*Notes*_
> >
> Trois façons de configurer les VTherms sont disponibles : > Trois façons de configurer les VTherms sont disponibles :
> 1. Chaque Versatile Thermostat est entièrement configurée de manière indépendante. Choisissez cette option si vous ne souhaitez avoir aucune configuration ou gestion centrale. > 1. Chaque Versatile Thermostat est entièrement configurée de manière indépendante. Choisissez cette option si vous ne souhaitez avoir aucune configuration ou gestion centrale.
@@ -285,32 +202,20 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
> 3. En plus de cette configuration centralisée, tous les VTherm peuvent être contrôlées par une seule entité de type `select`. Cette fonction est nommé `central_mode`. Cela permet de stopper / démarrer / mettre en hors gel / etc tous les VTherms en une seule fois. Pour chaque VTherm, l'utilisateur indique si il est concerné par ce `central_mode`. > 3. En plus de cette configuration centralisée, tous les VTherm peuvent être contrôlées par une seule entité de type `select`. Cette fonction est nommé `central_mode`. Cela permet de stopper / démarrer / mettre en hors gel / etc tous les VTherms en une seule fois. Pour chaque VTherm, l'utilisateur indique si il est concerné par ce `central_mode`.
<details>
<summary>Création d'un nouveau Versatile Thermostat</summary>
## Création d'un nouveau Versatile Thermostat ## Création d'un nouveau Versatile Thermostat
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
![image](images/add-an-integration.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/add-an-integration.png?raw=true)
puis
![image](images/config-main0.png)
La configuration peut être modifiée via la même interface. Sélectionnez simplement le thermostat à modifier, appuyez sur "Configurer" et vous pourrez modifier certains paramètres ou la configuration. La configuration peut être modifiée via la même interface. Sélectionnez simplement le thermostat à modifier, appuyez sur "Configurer" et vous pourrez modifier certains paramètres ou la configuration.
Suivez ensuite les étapes de configuration en sélectionnant dans le menu l'option à configurer. Suivez ensuite les étapes de configuration comme suit :
</details>
<details>
<summary>Choix des attributs de base</summary>
## Choix des attributs de base ## Choix des attributs de base
Choisisez le menu "Principaux attributs". ![image](/images/config-main0.png?raw=true)
![image](images/config-main.png) ![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)
@@ -323,19 +228,14 @@ Donnez les principaux attributs obligatoires :
9. la possibilité de controler le thermostat de façon centralisée. Cf [controle centralisé](#le-contrôle-centralisé), 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. 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](images/tips.png) _*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**,
> 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement. > 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement.
</details>
<details>
<summary>Sélectionnez des entités pilotées (sous-jacents)</summary>
## Sélectionnez des entités pilotées (sous-jacents)
## Sélectionnez des entités pilotées
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type `switch`, `climate` ou `number`. Seules les entités compatibles avec le type sont présentées. En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type `switch`, `climate` ou `number`. Seules les entités compatibles avec le type sont présentées.
> ![Astuce](images/tips.png) _*Comment choisir le type*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Comment choisir le type*_
> Le choix du type est important. Même si il toujours possible de le modifier ensuite via l'IHM de configuration, il est préférable de se poser les quelques questions suivantes : > Le choix du type est important. Même si il toujours possible de le modifier ensuite via l'IHM de configuration, il est préférable de se poser les quelques questions suivantes :
> 1. **quel type d'équipement je vais piloter ?** Dans l'ordre voici ce qu'il faut faire : > 1. **quel type d'équipement je vais piloter ?** Dans l'ordre voici ce qu'il faut faire :
> 1. si vous avez une vanne thermostatique (TRV) commandable dans Home Assistant via une entité de type ```number``` (par exemple une _Shelly TRV_), choisissez le type `over_valve`. C'est le type le plus direct et qui assure la meilleure régulation, > 1. si vous avez une vanne thermostatique (TRV) commandable dans Home Assistant via une entité de type ```number``` (par exemple une _Shelly TRV_), choisissez le type `over_valve`. C'est le type le plus direct et qui assure la meilleure régulation,
@@ -344,21 +244,18 @@ En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou
> 2. **quelle type de régulation je veux ?** Si l'équipement piloté possède son propre mécanisme de régulation (clim, certaine vanne TRV) et que cette régulation fonctionne bien, optez pour un ```over_climate``` > 2. **quelle type de régulation je veux ?** Si l'équipement piloté possède son propre mécanisme de régulation (clim, certaine vanne TRV) et que cette régulation fonctionne bien, optez pour un ```over_climate```
### Pour un thermostat de type ```thermostat_over_switch``` ### Pour un thermostat de type ```thermostat_over_switch```
![image](images/config-linked-entity.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
Certains équipements nécessitent d'être périodiquement sollicités pour empêcher un arrêt de sécurité. Connu sous le nom de "keep-alive" cette fonction est activable en entrant un nombre de secondes non nul dans le champ d'intervalle keep-alive du thermostat. Pour désactiver la fonction ou en cas de doute, laissez-le vide ou entrez zéro (valeur par défaut).
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme). L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour. Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
Exemple de déclenchement synchronisé : Exemple de déclenchement synchronisé :
![image](images/multi-switch-activation.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible. Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à On lorsqu'on doit étiendre l'équipement et à Off lorsqu'on doit l'allumer. Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à On lorsqu'on doit étiendre l'équipement et à Off lorsqu'on doit l'allumer.
### Pour un thermostat de type ```thermostat_over_climate```: ### Pour un thermostat de type ```thermostat_over_climate```:
![image](images/config-linked-entity2.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement. Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement.
@@ -381,7 +278,7 @@ La fonction d'auto-régulation se paramètre avec :
Ces trois paramètres permettent de moduler la régulation et éviter de multiplier les envois de régulation. Certains équipements comme les TRV, les chaudières n'aiment pas qu'on change la consigne de température trop souvent. Ces trois paramètres permettent de moduler la régulation et éviter de multiplier les envois de régulation. Certains équipements comme les TRV, les chaudières n'aiment pas qu'on change la consigne de température trop souvent.
> ![Astuce](images/tips.png) _*Conseil de mise en place*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Conseil de mise en place*_
> 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation, > 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation,
> 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée, > 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée,
> 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation, > 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation,
@@ -468,37 +365,6 @@ et bien sur, configurer le mode auto-régulation du VTherm en mode Expert. Tous
Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat). Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat).
#### Compensation de la température interne
Quelque fois, il arrive que le thermomètre interne du sous-jacent (TRV, climatisation, ...) soit tellement faux que l' auto-régulation ne suffise pas à réguler.
Cela arrive lorsque le thermomètre interne est trop près de la source de chaleur. La température interne monte alors beaucoup plus vite que la température de la pièce, ce qui génère des défauts dans la régulation.
Exemple :
1. la température de la pièce est 18°, la consigne est à 20°,
2. la température interne de l'équipement est de 22°,
3. si VTherm envoie 21° comme consigne (= 20° + 1° d'auto-regulation), alors l'équipement ne chauffera pas car sa température interne (22°) est au-dessus de la consigne (21°)
Pour palier à ça, une nouvelle option facultative a été ajoutée en version 5.4 : ![Utilisation de la température interne](images/config-use-internal-temp.png)
Lorsqu'elle est activée, cette fonction ajoutera l'écart entre la température interne et la température de la pièce à la consigne pour forcer le chauffage.
Dans l'exemple ci-dessus, l'écart est de +4° (22° - 18°), donc VTherm enverra 25° (21°+4°) à l'équipement le forçant ainsi à chauffer.
Cet écart est calculé pour chaque sous-jacent car chacun à sa propre température interne. Pensez à un VTherm qui serait relié à 3 TRV chacun avec sa température interne par exemple.
On obtient alors une auto-régulation bien plus efficace qui évite l'eccueil des gros écarts de température interne défaillante.
#### Synthèse de l'algorithme d'auto-régulation
L'algorithme d'auto-régulation peut être synthétisé comme suit:
1. initialiser la température cible comme la consigne du VTherm,
1. Si l'auto-régulation est activée,
1. calcule de la température régulée (valable pour un VTherm),
2. prendre cette température comme cible,
2. Pour chaque sous-jacent du VTherm,
1. Si "utiliser la température interne" est cochée,
1. calcule de l'écart (trv internal temp - room temp),
2. ajout de l'écart à la température cible,
3. envoie de la température cible ( = temp regulee + (temp interne - temp pièce)) au sous-jacent
#### Le mode auto-fan #### Le mode auto-fan
Ce mode introduit en 4.3 permet de forcer l'usage de la ventilation si l'écart de température est important. En effet, en activant la ventilation, la répartition se fait plus rapidement ce qui permet de gagner du temps dans l'atteinte de la température cible. Ce mode introduit en 4.3 permet de forcer l'usage de la ventilation si l'écart de température est important. En effet, en activant la ventilation, la répartition se fait plus rapidement ce qui permet de gagner du temps dans l'atteinte de la température cible.
@@ -509,21 +375,17 @@ Si votre équipement ne comprend pas le mode Turbo, le mode Forte` sera utilisé
Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie. Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie.
### Pour un thermostat de type ```thermostat_over_valve```: ### Pour un thermostat de type ```thermostat_over_valve```:
![image](images/config-linked-entity3.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes. Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme). L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible. Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
</details>
<details>
<summary>Configurez les coefficients de l'algorithme TPI</summary>
## Configurez les coefficients de l'algorithme TPI ## Configurez les coefficients de l'algorithme TPI
Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` et que vous sélectionnez l'option "TPI" vous menu, vous arriverez sur cette page : Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` vous arriverez sur cette page :
![image](images/config-tpi.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
Vous devez donner : Vous devez donner :
1. le coefficient coef_int de l'algorithme TPI, 1. le coefficient coef_int de l'algorithme TPI,
@@ -531,12 +393,11 @@ Vous devez donner :
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm). Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
</details>
<details> ## Configurer la température préréglée
<summary>Configurer les températures préréglées</summary> Cliquez sur 'Valider' sur la page précédente et vous y arriverez :
## Configurer les températures préréglées ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presets.png?raw=true)
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](#even-better-with-scheduler-component) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants : Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](#even-better-with-scheduler-component) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants :
- **Eco** : l'appareil est en mode d'économie d'énergie - **Eco** : l'appareil est en mode d'économie d'énergie
@@ -547,21 +408,14 @@ Le mode préréglé (preset) vous permet de préconfigurer la température cibl
**Aucun** est toujours ajouté dans la liste des modes, car c'est un moyen de ne pas utiliser les preset mais une **température manuelle** à la place. **Aucun** est toujours ajouté dans la liste des modes, car c'est un moyen de ne pas utiliser les preset mais une **température manuelle** à la place.
Les pré-réglages se font (depuis v6.0) directement depuis les entités du VTherm ou de la configuration centrale si vous utilisez la configuration centrale. > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> ![Astuce](images/tips.png) _*Notes*_
> 1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible. > 1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible.
> 2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management). > 2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management).
> 3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management). > 3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
> 4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai > 4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
> 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front > 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
</details>
<details>
<summary>Configurer les portes/fenêtres en allumant/éteignant les thermostats</summary>
## Configurer les portes/fenêtres en allumant/éteignant les thermostats ## Configurer les portes/fenêtres en allumant/éteignant les thermostats
Vous devez avoir choisi la fonctionnalité ```Avec détection des ouvertures``` dans la première page pour arriver sur cette page. Vous devez avoir choisi la fonctionnalité ```Avec détection des ouvertures``` dans la première page pour arriver sur cette page.
La détecttion des ouvertures peut se faire de 2 manières: La détecttion des ouvertures peut se faire de 2 manières:
1. soit avec un capteur placé sur l'ouverture (mode capteur), 1. soit avec un capteur placé sur l'ouverture (mode capteur),
@@ -569,7 +423,7 @@ La détecttion des ouvertures peut se faire de 2 manières:
### Le mode capteur ### Le mode capteur
En mode capteur, vous devez renseigner les informations suivantes: En mode capteur, vous devez renseigner les informations suivantes:
![image](images/config-window-sensor.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-sensor.png?raw=true)
1. un identifiant d'entité d'un **capteur de fenêtre/porte**. Cela devrait être un binary_sensor ou un input_boolean. L'état de l'entité doit être 'on' lorsque la fenêtre est ouverte ou 'off' lorsqu'elle est fermée 1. un identifiant d'entité d'un **capteur de fenêtre/porte**. Cela devrait être un binary_sensor ou un input_boolean. L'état de l'entité doit être 'on' lorsque la fenêtre est ouverte ou 'off' lorsqu'elle est fermée
2. un **délai en secondes** avant tout changement. Cela permet d'ouvrir rapidement une fenêtre sans arrêter le chauffage. 2. un **délai en secondes** avant tout changement. Cela permet d'ouvrir rapidement une fenêtre sans arrêter le chauffage.
@@ -577,7 +431,7 @@ En mode capteur, vous devez renseigner les informations suivantes:
### Le mode auto ### Le mode auto
En mode auto, la configuration est la suivante: En mode auto, la configuration est la suivante:
![image](images/config-window-auto.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-auto.png?raw=true)
1. un seuil de détection en degré par minute. Lorsque la température chute au delà de ce seuil, le thermostat s'éteindra. Plus cette valeur est faible et plus la détection sera rapide (en contre-partie d'un risque de faux positif), 1. un seuil de détection en degré par minute. Lorsque la température chute au delà de ce seuil, le thermostat s'éteindra. Plus cette valeur est faible et plus la détection sera rapide (en contre-partie d'un risque de faux positif),
2. un seuil de fin de détection en degré par minute. Lorsque la chute de température repassera au-dessus cette valeur, le thermostat se remettra dans le mode précédent (mode et preset), 2. un seuil de fin de détection en degré par minute. Lorsque la chute de température repassera au-dessus cette valeur, le thermostat se remettra dans le mode précédent (mode et preset),
@@ -589,28 +443,23 @@ Pour régler les seuils il est conseillé de commencer avec les valeurs de réf
- durée max : 60 min. - durée max : 60 min.
Un nouveau capteur "slope" a été ajouté pour tous les thermostats. Il donne la pente de la courbe de température en °C/min (ou °K/min). Cette pente est lissée et filtrée pour éviter les valeurs abérrantes des thermomètres qui viendraient pertuber la mesure. Un nouveau capteur "slope" a été ajouté pour tous les thermostats. Il donne la pente de la courbe de température en °C/min (ou °K/min). Cette pente est lissée et filtrée pour éviter les valeurs abérrantes des thermomètres qui viendraient pertuber la mesure.
![image](images/temperature-slope.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/temperature-slope.png?raw=true)
Pour bien régler il est conseillé d'affocher sur un même graphique historique la courbe de température et la pente de la courbe (le "slope") : Pour bien régler il est conseillé d'affocher sur un même graphique historique la courbe de température et la pente de la courbe (le "slope") :
![image](images/window-auto-tuning.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/window-auto-tuning.png?raw=true)
Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé. Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé.
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. Si vous souhaitez utiliser **plusieurs capteurs de porte/fenêtre** pour automatiser votre thermostat, créez simplement un groupe avec le comportement habituel (https://www.home-assistant.io/integrations/binary_sensor.group/) > 1. Si vous souhaitez utiliser **plusieurs capteurs de porte/fenêtre** pour automatiser votre thermostat, créez simplement un groupe avec le comportement habituel (https://www.home-assistant.io/integrations/binary_sensor.group/)
> 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide, > 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
> 3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps, > 3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
> 4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...) > 4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
</details>
<details>
<summary>Configurer le mode d'activité ou la détection de mouvement</summary>
## Configurer le mode d'activité ou la détection de mouvement ## Configurer le mode d'activité ou la détection de mouvement
Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez : Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
![image](images/config-motion.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true)
Nous allons maintenant voir comment configurer le nouveau mode Activité. Nous allons maintenant voir comment configurer le nouveau mode Activité.
Ce dont nous avons besoin: Ce dont nous avons besoin:
@@ -630,41 +479,32 @@ Alors imaginons que nous voulions avoir le comportement suivant :
Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ». Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ».
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique 1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
</details>
<details>
<summary>Configurer la gestion de la puissance</summary>
## Configurer la gestion de la puissance ## Configurer la gestion de la puissance
Si vous avez choisi la fonctionnalité ```Avec détection de la puissance```, cliquez sur 'Valider' sur la page précédente et vous arriverez ici : Si vous avez choisi la fonctionnalité ```Avec détection de la puissance```, cliquez sur 'Valider' sur la page précédente et vous arriverez ici :
![image](images/config-power.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true)
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées. Donnez un **capteur à la consommation électrique actuelle de votre maison**, un **capteur à la puissance max** qu'il ne faut pas dépasser, la **consommation électrique totale des équipements du VTherm** (en étape 1 de la configuration) et l'algorithme ne démarrera pas un radiateur si la puissance maximale sera dépassée après le démarrage du radiateur. Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées. Donnez un **capteur à la consommation électrique actuelle de votre maison**, un **capteur à la puissance max** qu'il ne faut pas dépasser, la **consommation électrique totale des équipements du VTherm** (en étape 1 de la configuration) et l'algorithme ne démarrera pas un radiateur si la puissance maximale sera dépassée après le démarrage du radiateur.
Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple). Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple).
Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez. Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement. > 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
> 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation. > 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
> 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés. > 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
> 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide > 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
> 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances. > 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
</details>
<details>
<summary>Configurer la présence (ou l'absence)</summary>
## Configurer la présence (ou l'absence)
## Configurer la présence ou l'occupation
Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature). Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature).
Pour configurer la présence remplissez ce formulaire : Pour configurer la présence remplissez ce formulaire :
![image](images/config-presence.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true)
Pour cela, vous devez configurer : Pour cela, vous devez configurer :
1. Un **capteur d'occupation** dont l'état doit être 'on' ou 'home' si quelqu'un est présent ou 'off' ou 'not_home' sinon, 1. Un **capteur d'occupation** dont l'état doit être 'on' ou 'home' si quelqu'un est présent ou 'off' ou 'not_home' sinon,
@@ -676,20 +516,15 @@ Si le mode AC est utilisé, vous pourrez aussi configurer les températures lors
ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de présence. Ils ne sont pas reconnus comme un capteur de présence. Vous devez utiliser, un template comme décrit ici [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence). ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de présence. Ils ne sont pas reconnus comme un capteur de présence. Vous devez utiliser, un template comme décrit ici [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence).
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle, > 1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
> 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents. > 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
</details>
<details>
<summary>Configuration avancée</summary>
## Configuration avancée ## Configuration avancée
Ces paramètres permettent d'affiner le réglage du thermostat. Ces paramètres permettent d'affiner le réglage du thermostat.
Le formulaire de configuration avancée est le suivant : Le formulaire de configuration avancée est le suivant :
![image](images/config-advanced.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-advanced.png?raw=true)
Le premier délai (minimal_activation_delay_sec) en secondes est le délai minimum acceptable pour allumer le chauffage. Lorsque le calcul donne un délai de mise sous tension inférieur à cette valeur, le chauffage reste éteint. Le premier délai (minimal_activation_delay_sec) en secondes est le délai minimum acceptable pour allumer le chauffage. Lorsque le calcul donne un délai de mise sous tension inférieur à cette valeur, le chauffage reste éteint.
@@ -711,19 +546,14 @@ Par défaut, le thermomètre extérieur peut déclencher une mise en sécurité
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](/images/tips.png?raw=true) _*Notes*_
> 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente, > 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security", > 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage, > 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``, > 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security. > 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
</details>
<details>
<summary>Le contrôle centralisé</summary>
## Le contrôle centralisé ## Le contrôle centralisé
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. 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 : Cette entité se présente sous la forme d'une liste de choix qui contient les choix suivants :
@@ -736,14 +566,9 @@ Cette entité se présente sous la forme d'une liste de choix qui contient les c
Il est donc possible de contrôler tous les VTherms (que ceux que l'on désigne explicitement) avec un seul contrôle. 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 : Exemple de rendu :
![central_mode](images/central_mode.png) ![central_mode](/images/central_mode.png?raw=true)
</details>
<details>
<summary>Le contrôle d'une chaudière centrale</summary>
## Le contrôle d'une chaudière centrale ## Le contrôle d'une chaudière centrale
Depuis la release 5.3, vous avez la possibilité de contrôler une chaudière centralisée. A partir du moment où il est possible de déclencher ou stopper cette chaudière depuis Home Assistant, alors Versatile Thermostat va pouvoir la commander directement. Depuis la release 5.3, vous avez la possibilité de contrôler une chaudière centralisée. A partir du moment où il est possible de déclencher ou stopper cette chaudière depuis Home Assistant, alors Versatile Thermostat va pouvoir la commander directement.
Le principe mis en place est globalement le suivant : Le principe mis en place est globalement le suivant :
@@ -759,16 +584,16 @@ Le principe mis en place est globalement le suivant :
Vous avez donc en permanence, les informations qui permettent de piloter et régler le déclenchement de la chaudière. Vous avez donc en permanence, les informations qui permettent de piloter et régler le déclenchement de la chaudière.
Toutes ces entités sont rattachés au service de configuration centrale : Toutes ces entités sont rattachés au service de configuration centrale :
![Les entités pilotant la chaudière](images/entitites-central-boiler.png) ![Les entités pilotant la chaudière](/images/entitites-central-boiler.png?raw=true)
### Configuration ### Configuration
Pour configurer cette fonction, vous devez avoir une configuration centralisée (cf. [Configuration](#configuration)) et cochez la case 'Ajouter une chuadière centrale' : Pour configurer cette fonction, vous devez avoir une configuration centralisée (cf. [Configuration](#configuration)) et cochez la case 'Ajouter une chuadière centrale' :
![Ajout d'une chaudière centrale](images/config-central-boiler-1.png) ![Ajout d'une chaudière centrale](/images/config-central-boiler-1.png?raw=true)
Sur la page suivante vous pouvez donner la configuration des services à appeler lors de l'allumage / extinction de la chaudière : Sur la page suivante vous pouvez donner la configuration des services à appeler lors de l'allumage / extinction de la chaudière :
![Ajout d'une chaudière centrale](images/config-central-boiler-2.png) ![Ajout d'une chaudière centrale](/images/config-central-boiler-2.png?raw=true)
Les services se configurent comme indiqués dans la page : Les services se configurent comme indiqués dans la page :
1. le format général est `entity_id/service_id[/attribut:valeur]` (où `/attribut:valeur` est facultatif), 1. le format général est `entity_id/service_id[/attribut:valeur]` (où `/attribut:valeur` est facultatif),
@@ -791,11 +616,11 @@ Exemple:
Sous "Outils de développement / Service" : Sous "Outils de développement / Service" :
![Configuration du service](images/dev-tools-turnon-boiler-1.png) ![Configuration du service](/images/dev-tools-turnon-boiler-1.png?raw=true)
En mode yaml : En mode yaml :
![Configuration du service](images/dev-tools-turnon-boiler-2.png) ![Configuration du service](/images/dev-tools-turnon-boiler-2.png?raw=true)
Le service à configurer est alors le suivant: `climate.empty_thermostast/climate.set_hvac_mode/hvac_mode:heat` (notez la suppression du blanc dans `hvac_mode:heat`) Le service à configurer est alors le suivant: `climate.empty_thermostast/climate.set_hvac_mode/hvac_mode:heat` (notez la suppression du blanc dans `hvac_mode:heat`)
@@ -840,12 +665,8 @@ context:
### Avertissement ### Avertissement
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> Le contrôle par du logiciel ou du matériel de type domotique d'une chaudière centrale peut induire des risques pour son bon fonctionnement. Assurez-vous avant d'utiliser ces fonctions, que votre chaudière possède bien des fonctions de sécurité et que celles-ci fonctionnent. Allumer une chaudière si tous les robinets sont fermés peut générer de la sur-pression par exemple. > Le contrôle par du logiciel ou du matériel de type domotique d'une chaudière centrale peut induire des risques pour son bon fonctionnement. Assurez-vous avant d'utiliser ces fonctions, que votre chaudière possède bien des fonctions de sécurité et que celles-ci fonctionnent. Allumer une chaudière si tous les robinets sont fermés peut générer de la sur-pression par exemple.
</details>
<details>
<summary>Synthèse des paramètres</summary>
## Synthèse des paramètres ## Synthèse des paramètres
@@ -868,7 +689,6 @@ context:
| ``heater_entity2_id`` | 2ème radiateur | X | - | - | - | | ``heater_entity2_id`` | 2ème radiateur | X | - | - | - |
| ``heater_entity3_id`` | 3ème radiateur | X | - | - | - | | ``heater_entity3_id`` | 3ème radiateur | X | - | - | - |
| ``heater_entity4_id`` | 4ème radiateur | X | - | - | - | | ``heater_entity4_id`` | 4ème radiateur | X | - | - | - |
| ``heater_keep_alive`` | Intervalle de rafraichissement du switch | X | - | - | - |
| ``proportional_function`` | Algorithme | X | - | - | - | | ``proportional_function`` | Algorithme | X | - | - | - |
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - | - | | ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - | - |
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - | - | | ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - | - |
@@ -881,7 +701,13 @@ context:
| ``ac_mode`` | utilisation de l'air conditionné (AC) ? | X | X | X | - | | ``ac_mode`` | utilisation de l'air conditionné (AC) ? | X | X | X | - |
| ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - | X | X | | ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - | X | X |
| ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - | X | X | | ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - | X | X |
| ``frost_temp`` | Température en preset Hors-gel | X | X | X | X | | ``frost_tp`` | Température en preset Hors-gel | X | X | X | X |
| ``eco_temp`` | Température en preset Eco | X | X | X | X |
| ``comfort_temp`` | Température en preset Confort | X | X | X | X |
| ``boost_temp`` | Température en preset Boost | X | X | X | X |
| ``eco_ac_temp`` | Température en preset Eco en mode AC | X | X | X | X |
| ``comfort_ac_temp`` | Température en preset Confort en mode AC | X | X | X | X |
| ``boost_ac_temp`` | Température en preset Boost en mode AC | X | X | X | X |
| ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X | X | - | | ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X | X | - |
| ``window_delay`` | Délai avant extinction (secondes) | X | X | X | X | | ``window_delay`` | Délai avant extinction (secondes) | X | X | X | X |
| ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X | X | | ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X | X |
@@ -896,6 +722,13 @@ context:
| ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X | X | X | | ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X | X | X |
| ``power_temp`` | Température si délestaqe | X | X | X | X | | ``power_temp`` | Température si délestaqe | X | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - | | ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - |
| ``frost_ay_temp`` | Température en preset Hors-gel en cas d'absence | X | X | X | X |
| ``eco_away_temp`` | Température en preset Eco en cas d'absence | X | X | X | X |
| ``comfort_away_temp`` | Température en preset Comfort en cas d'absence | X | X | X | X |
| ``boost_away_temp`` | Température en preset Boost en cas d'absence | X | X | X | X |
| ``eco_ac_away_temp`` | Température en preset Eco en cas d'absence en mode AC | X | X | X | X |
| ``comfort_ac_away_temp`` | Température en preset Comfort en cas d'absence en mode AC | X | X | X | X |
| ``boost_ac_away_temp`` | Température en preset Boost en cas d'absence en mode AC | X | X | X | X |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X | | ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X |
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X | | ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X | | ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
@@ -904,12 +737,10 @@ context:
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - | | ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
| ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - | - | | ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - | - |
| ``auto_fan_mode`` | Mode de ventilation automatique | - | X | - | - | | ``auto_fan_mode`` | Mode de ventilation automatique | - | X | - | - |
| ``auto_regulation_use_device_temp`` | Utilisation de la température interne du sous-jacent | - | X | - | - | | ``add_central_boiler_control`` | Ajout du controle d'une chaudière centrale | - | - | - | X |
| ``use_central_boiler_feature`` | Ajout du controle d'une chaudière centrale | - | - | - | X |
| ``central_boiler_activation_service`` | Service d'activation de la chaudière | - | - | - | X | | ``central_boiler_activation_service`` | Service d'activation de la chaudière | - | - | - | X |
| ``central_boiler_deactivation_service`` | Service de desactivation de la chaudière | - | - | - | X | | ``central_boiler_deactivation_service`` | Service de desactivation de la chaudière | - | - | - | X |
| ``used_by_controls_central_boiler`` | Indique si le VTherm contrôle la chaudière centrale | X | X | X | - | | ``used_by_controls_central_boiler`` | Indique si le VTherm contrôle la chaudière centrale | X | X | X | - |
</details>
# Exemples de réglage # Exemples de réglage
@@ -985,7 +816,7 @@ Voir quelques situations à [examples](#some-results).
Avec le thermostat sont disponibles des capteurs qui permettent de visualiser les alertes et l'état interne du thermostat. Ils sont disponibles dans les entités de l'appareil associé au thermostat : Avec le thermostat sont disponibles des capteurs qui permettent de visualiser les alertes et l'état interne du thermostat. Ils sont disponibles dans les entités de l'appareil associé au thermostat :
![image](images/thermostat-sensors.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/thermostat-sensors.png?raw=true)
Dans l'ordre, il y a : Dans l'ordre, il y a :
1. l'entité principale climate de commande du thermostat, 1. l'entité principale climate de commande du thermostat,
@@ -1018,7 +849,7 @@ frontend:
``` ```
et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça : et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça :
![image](images/colored-thermostat-sensors.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/colored-thermostat-sensors.png?raw=true)
# Services # Services
@@ -1064,7 +895,7 @@ target:
entity_id: climate.my_thermostat entity_id: climate.my_thermostat
``` ```
> ![Astuce](images/tips.png) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- après un redémarrage, les préréglages sont réinitialisés à la température configurée. Si vous souhaitez que votre changement soit permanent, vous devez modifier le préréglage de la température dans la configuration de l'intégration. - après un redémarrage, les préréglages sont réinitialisés à la température configurée. Si vous souhaitez que votre changement soit permanent, vous devez modifier le préréglage de la température dans la configuration de l'intégration.
## Modifier les paramètres de sécurité ## Modifier les paramètres de sécurité
@@ -1119,7 +950,7 @@ Vous pouvez très facilement capter ses évènements dans une automatisation par
# Attributs personnalisés # Attributs personnalisés
Pour régler l'algorithme, vous avez accès à tout le contexte vu et calculé par le thermostat via des attributs dédiés. Vous pouvez voir (et utiliser) ces attributs dans l'IHM "Outils de développement / états" de HA. Entrez votre thermostat et vous verrez quelque chose comme ceci : Pour régler l'algorithme, vous avez accès à tout le contexte vu et calculé par le thermostat via des attributs dédiés. Vous pouvez voir (et utiliser) ces attributs dans l'IHM "Outils de développement / états" de HA. Entrez votre thermostat et vous verrez quelque chose comme ceci :
![image](images/dev-tools-climate.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/dev-tools-climate.png?raw=true)
Les attributs personnalisés sont les suivants : Les attributs personnalisés sont les suivants :
@@ -1170,23 +1001,23 @@ Les attributs personnalisés sont les suivants :
# Quelques résultats # Quelques résultats
**Convergence de la température vers la cible configurée par preset:** **Convergence de la température vers la cible configurée par preset:**
![image](images/results-1.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-1.png?raw=true)
[Cycle de marche/arrêt calculé par l'intégration :](https://) [Cycle de marche/arrêt calculé par l'intégration :](https://)
![image](images/results-2.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-2.png?raw=true)
**Coef_int trop élevé (oscillations autour de la cible)** **Coef_int trop élevé (oscillations autour de la cible)**
![image](images/results-3.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-3.png?raw=true)
**Évolution du calcul de l'algorithme** **Évolution du calcul de l'algorithme**
![image](images/results-4.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-4.png?raw=true)
Voir le code de ce composant [[ci-dessous](#even-better-with-apex-chart-to-tune-your-thermostat)] Voir le code de ce composant [[ci-dessous](#even-better-with-apex-chart-to-tune-your-thermostat)]
**Thermostat finement réglé** **Thermostat finement réglé**
Merci [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) ! Merci [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) !
On peut voir une stabilité autour de la température cible (consigne) et lorsqu'à cible le on_percent (puissance) est proche de 0,3 ce qui semble une très bonne valeur. On peut voir une stabilité autour de la température cible (consigne) et lorsqu'à cible le on_percent (puissance) est proche de 0,3 ce qui semble une très bonne valeur.
![image](images/results-fine-tuned.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-fine-tuned.png?raw=true)
Enjoy ! Enjoy !
@@ -1225,7 +1056,7 @@ J'espère que cet exemple vous aidera, n'hésitez pas à me faire part de vos re
## Encore bien mieux avec la custom:simple-thermostat front integration ## Encore bien mieux avec la custom:simple-thermostat front integration
Le ``custom:simple-thermostat`` [ici](https://github.com/nervetattoo/simple-thermostat) est une excellente intégration qui permet une certaine personnalisation qui s'adapte bien à ce thermostat. Le ``custom:simple-thermostat`` [ici](https://github.com/nervetattoo/simple-thermostat) est une excellente intégration qui permet une certaine personnalisation qui s'adapte bien à ce thermostat.
Vous pouvez avoir quelque chose comme ça très facilement ![image](images/simple-thermostat.png) Vous pouvez avoir quelque chose comme ça très facilement ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/simple-thermostat.png?raw=true)
Exemple de configuration : Exemple de configuration :
``` ```
@@ -1268,7 +1099,7 @@ Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour
} }
{% endif %} {% endif %}
``` ```
![image](images/custom-css-thermostat.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true)
## Toujours mieux avec Plotly pour régler votre thermostat ## Toujours mieux avec Plotly pour régler votre thermostat
Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Plotly uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) : Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Plotly uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) :
@@ -1341,7 +1172,7 @@ Remplacez les valeurs entre [[ ]] par les votres.
Exemple de courbes obtenues avec Plotly : Exemple de courbes obtenues avec Plotly :
![image](images/plotly-curves.png) ![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.
@@ -1437,11 +1268,7 @@ Si vous souhaitez contribuer, veuillez lire les [directives de contribution](CON
# Dépannages # Dépannages
<details>
<summary>Utilisation d'un Heatzy</summary>
## Utilisation d'un Heatzy ## Utilisation d'un Heatzy
L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virtuel sur ce modèle : L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virtuel sur ce modèle :
``` ```
- platform: template - platform: template
@@ -1470,10 +1297,6 @@ L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virt
preset_mode: "eco" preset_mode: "eco"
``` ```
Merci à @gael pour cet exemple. Merci à @gael pour cet exemple.
</details>
<details>
<summary>Utilisation d'un radiateur avec un fil pilote</summary>
## Utilisation d'un radiateur avec un fil pilote ## Utilisation d'un radiateur avec un fil pilote
Comme pour le Heatzy ci-dessus vous pouvez utiliser un switch virtuel qui va changer le preset de votre radiateur en fonction de l'état d'allumage du VTherm. Comme pour le Heatzy ci-dessus vous pouvez utiliser un switch virtuel qui va changer le preset de votre radiateur en fonction de l'état d'allumage du VTherm.
@@ -1495,21 +1318,10 @@ Exemple :
icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}" icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}"
``` ```
</details>
<details>
<summary>Seul le premier radiateur chauffe</summary>
## Seul le premier radiateur chauffe ## Seul le premier radiateur chauffe
En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation. En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation.
Cela est tout à fait normal et voulu. C'est décrit ici : [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch) Cela est tout à fait normal et voulu. C'est décrit ici : [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
</details>
<details>
<summary>Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne</summary>
## Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne ## Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne
### Type `over_switch` ou `over_valve` ### Type `over_switch` ou `over_valve`
@@ -1521,10 +1333,7 @@ Avec un VTherm de type `over_climate`, la régulation est faite par le `climate`
Exemple de discussion autour de ces sujets: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278) Exemple de discussion autour de ces sujets: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278)
Pour s'en sortir, VTherm est équipé d'une fonction nommée auto-régulation qui permet d'adapter la consigne envoyée au sous-jacent jusqu'à ce que la consigne soit respectée. Cette fonction permet de compenser le biais de mesure des thermomètres internes. Si le biais est important la régulation doit être importante. Voir [L'auto-régulation](#lauto-régulation) pour configurer l'auto-régulation. Pour s'en sortir, VTherm est équipé d'une fonction nommée auto-régulation qui permet d'adapter la consigne envoyée au sous-jacent jusqu'à ce que la consigne soit respectée. Cette fonction permet de compenser le biais de mesure des thermomètres internes. Si le biais est important la régulation doit être importante. Voir [L'auto-régulation](#lauto-régulation) pour configurer l'auto-régulation.
</details>
<details>
<summary>Régler les paramètres de détection d'ouverture de fenêtre en mode auto</summary>
## Régler les paramètres de détection d'ouverture de fenêtre en mode auto ## Régler les paramètres de détection d'ouverture de fenêtre en mode auto
@@ -1545,10 +1354,6 @@ versatile_thermostat:
``` ```
Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les utiliser que si vous savez ce que vous faites et que vos mesures de température ne sont pas déjà lisses. Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les utiliser que si vous savez ce que vous faites et que vos mesures de température ne sont pas déjà lisses.
</details>
<details>
<summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
## Pourquoi mon Versatile Thermostat se met en Securite ? ## Pourquoi mon Versatile Thermostat se met en Securite ?
Le mode sécurité n'est possible que sur les VTherm `over_switch` et `over_valve`. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`. Le mode sécurité n'est possible que sur les VTherm `over_switch` et `over_valve`. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
@@ -1561,11 +1366,11 @@ Tous ces paramètres se règlent dans la dernière page de la configuration du V
Le premier symptôme est une température anormalement basse avec un temps de chauffe faible à chaque cycle et régulier. Le premier symptôme est une température anormalement basse avec un temps de chauffe faible à chaque cycle et régulier.
Exemple: Exemple:
[security mode](images/security-mode-symptome1.png) [security mode](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome1.png?raw=true)
Si vous avez installé la carte [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), le VTherm en question aura cette forme là : Si vous avez installé la carte [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), le VTherm en question aura cette forme là :
[security mode UI Card](images/security-mode-symptome2.png) [security mode UI Card](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome2.png?raw=true)
Vous pouvez aussi vérifier dans les attributs du VTherm les dates de réception des différentes dates. **Les attributs sont disponibles dans les Outils de développement / Etats**. Vous pouvez aussi vérifier dans les attributs du VTherm les dates de réception des différentes dates. **Les attributs sont disponibles dans les Outils de développement / Etats**.
@@ -1596,13 +1401,8 @@ Cela va dépendre de la cause du problème :
2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile. 2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`, 3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées. 4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
</details>
<details>
<summary>Utilisation d'un groupe de personnes comme capteur de présence</summary>
## Utilisation d'un groupe de personnes comme capteur de présence ## Utilisation d'un groupe de personnes comme capteur de présence
Malheureusement, les groupes de personnes ne sont pas reconnus comme des capteurs de présence. On ne peut donc pas les utiliser directement dans VTherm. Malheureusement, les groupes de personnes ne sont pas reconnus comme des capteurs de présence. On ne peut donc pas les utiliser directement dans VTherm.
Le contournement est de créer un template de binary_sensor avec le code suivant : Le contournement est de créer un template de binary_sensor avec le code suivant :
@@ -1623,21 +1423,6 @@ Fichier `configuration.yaml`:
template: !include templates.yaml template: !include templates.yaml
... ...
``` ```
</details>
<details>
<summary>Activer les logs du Versatile Thermostat</summary>
## Activer les logs du Versatile Thermostat
Des fois, vous aurez besoin d'activer les logs pour afiner les analyses. Pour cela, éditer le fichier `logger.yaml` de votre configuration et configurer les logs comme suit :
```
default: xxxx
logs:
custom_components.versatile_thermostat: info
```
Vous devez recharger la configuration yaml (Outils de dev / Yaml / Toute la configuration Yaml) ou redémarrer Home Assistant pour que ce changement soit pris en compte.
</details>
*** ***

388
README.md
View File

@@ -4,16 +4,11 @@
[![hacs][hacs_badge]][hacs] [![hacs][hacs_badge]][hacs]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
![Tip](images/icon.png) ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](images/tips.png) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-). > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
- [Changes in version 6.0](#changes-in-version-60) - [Major changes in version 5.0](#major-changes-in-version-50)
- [Temperature entities for presets](#temperature-entities-for-presets)
- [In the case of a central configuration](#in-the-case-of-a-central-configuration)
- [Redesign of the configuration menu](#redesign-of-the-configuration-menu)
- [The 'Incomplete configuration' and 'Finalize' menu options](#the-incomplete-configuration-and-finalize-menu-options)
- [Changements dans la version 5.0](#changements-dans-la-version-50)
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee) - [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use) - [When to use / not use](#when-to-use--not-use)
- [Incompatibilities](#incompatibilities) - [Incompatibilities](#incompatibilities)
@@ -29,8 +24,6 @@
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate) - [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
- [Self-regulation](#self-regulation) - [Self-regulation](#self-regulation)
- [Self-regulation in Expert mode](#self-regulation-in-expert-mode) - [Self-regulation in Expert mode](#self-regulation-in-expert-mode)
- [Internal temperature compensation](#internal-temperature-compensation)
- [synthesis of the self-regulation algorithm](#synthesis-of-the-self-regulation-algorithm)
- [Auto-fan mode](#auto-fan-mode) - [Auto-fan mode](#auto-fan-mode)
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve) - [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients) - [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
@@ -48,8 +41,8 @@
- [How to find the right service?](#how-to-find-the-right-service) - [How to find the right service?](#how-to-find-the-right-service)
- [The events](#the-events) - [The events](#the-events)
- [Warning](#warning) - [Warning](#warning)
- [Parameter summary](#parameter-summary) - [Parameters synthesis](#parameters-synthesis)
- [Tuning examples](#tuning-examples) - [Examples tuning](#examples-tuning)
- [Electrical heater](#electrical-heater) - [Electrical heater](#electrical-heater)
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system) - [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
- [Temperature sensor will battery](#temperature-sensor-will-battery) - [Temperature sensor will battery](#temperature-sensor-will-battery)
@@ -86,20 +79,12 @@
- [How can I be notified when this happens?](#how-can-i-be-notified-when-this-happens) - [How can I be notified when this happens?](#how-can-i-be-notified-when-this-happens)
- [How to repair?](#how-to-repair) - [How to repair?](#how-to-repair)
- [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor) - [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor)
- [Enable Versatile Thermostat logs](#enable-versatile-thermostat-logs)
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](images/new-icon.png) _*Latest releases*_ >![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 6.0**: > * **Release 5.4**: Added a temperature step [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311). Added some regulation thresholdfor `over_valve` VTherm in order to avoid drowing the battery of TRV devices [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338).
> - Added entities from the Number domain to configure preset temperatures [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> - Complete redesign of the configuration menu to remove temperatures and use a menu instead of a configuration tunnel [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> * **Release 5.4**:
> - Added temperature step [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311),
> - addition of regulation thresholds for the `over_valve` to avoid draining the TRV battery too much [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
> - added an option allowing the internal temperature of a TRV to be used to force self-regulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
> - added a keep-alive function for VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
> * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343) > * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158). > * **Release 5.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.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
@@ -125,78 +110,8 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter. > * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
</details> </details>
# Changes in version 6.0 # Major changes in version 5.0
![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true)
## Temperature entities for presets
Preset temperatures are now directly accessible in the form of entities linked to VTherm.
Example :
![Temperature entities](images/temp-entities-1.png)
The Boost, Comfort, Eco and Frost Protection entities allow you to directly adjust the temperatures of these presets without having to reconfigure the VTHerm in the configuration screens.
These modifications persist after a restart and are taken into account immediately by VTherm.
Depending on the functions activated, the list of temperatures may be more or less complete:
1. If presence management is activated, absence presets are created. They are suffixed with 'abs' for absence,
2. If air conditioning management (AC Mode) is activated, air conditioning mode presets are created. They are suffixed with 'clim' for air conditioning. Only the Frost protection preset has no equivalent in air conditioning mode,
3. The different absent and air conditioning combinations can be created depending on the configuration of the VTherm
If a VTherm uses the presets of the central configuration, these entities are not created, because the temperatures of the presets are managed by the central configuration.
### In the case of a central configuration
If you have configured a central configuration, this also has its own presets which meet the same rules as stated above.
Example of a central configuration with presence management and AC (air conditioning) mode:
![Temperature entities](images/temp-entities-2.png)
In the case of a change of a central configuration temperature, all VTherms that use this preset are immediately updated.
## Redesign of the configuration menu
The configuration menu has been completely revised. It dynamically adapts to the user's choices and allows direct access to the settings of the desired function without having to go through the entire configuration tunnel.
To create a new VTherm, you will first need to choose the type of VTherm:
![VTherm choice](images/config-main0.png)
Then, you now access the following configuration menu:
![VTherm menu](images/config-menu.png)
Each part to be configured is directly accessible, without having to go through the entire configuration tunnel as before.
You will note the menu option named `Functions` which allows you to choose which functions will be implemented for this VTherm:
![VTherm features](images/config-features.png)
Depending on your choices, the main menu will adapt to add the necessary options.
Example of menu with all functions checked:
![VTherm menu](images/config-menu-all-options.png)
You can see that the 'Opening detection', 'Motion detection', 'Power management' and 'Presence management' options have been added. You can then configure them.
### The 'Incomplete configuration' and 'Finalize' menu options
The last menu option is special. It allows you to validate the creation of the VTherm when all the functions have been correctly configured.
If one option is not configured correctly, the last option is:
![Incomplete configuration](images/config-not-complete.png)
Its selection does nothing but prevents you from finalizing the creation (resp. modification) of the VTherm.
**You must then search in the options which one is missing**.
Once all configuration is valid, the last option changes to:
![Complete configuration](images/config-complete.png)
Click on this option to create (resp. modify) the VTherm:
![Configuration Complete](images/config-terminate.png)
<details>
<summary>Changements dans la version 5.0</summary>
# Changements dans la version 5.0
You can now define a central configuration which will allow you to share certain attributes on all your VTherms (or only part of them). To use this possibility, you must: You can now define a central configuration which will allow you to share certain attributes on all your VTherms (or only part of them). To use this possibility, you must:
1. Create a VTherm of type “Central Configuration”, 1. Create a VTherm of type “Central Configuration”,
@@ -209,10 +124,11 @@ The configurable attributes in the central configuration are listed here: [Param
When changing the central configuration, all VTherms will be reloaded to take these changes into account. When changing the central configuration, all VTherms will be reloaded to take these changes into account.
Consequently, the entire configuration phase of a VTherm has been profoundly modified to be able to use the central configuration or overload the values of the central configuration with values specific to the VTherm being configured. Consequently, the entire configuration phase of a VTherm has been profoundly modified to be able to use the central configuration or overload the values of the central configuration with values specific to the VTherm being configured.
</details>
**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, @MattG, @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R. for the beers. It's very nice and encourages me to continue! Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco 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:
@@ -235,7 +151,7 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation. 2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation.
3. Thermostat of type Heatzy which doesn't supports the set_temperature command. 3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine. 4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
5. TRV of type Aqara SRTS-A01 and MOES TV01-ZB which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally. 5. TRV of type Aqara SRTS-A01 which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
# Why another thermostat implementation ? # Why another thermostat implementation ?
@@ -278,7 +194,7 @@ This component named __Versatile thermostat__ manage the following use cases :
-- VTherm = Versatile Thermostat in the remainder of this document -- -- VTherm = Versatile Thermostat in the remainder of this document --
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> >
> Three ways to configure VTherms are available: > Three ways to configure VTherms are available:
> 1. Each Versatile Thermostat is completely independently configured. Choose this option if you do not want to have any central configuration or management. > 1. Each Versatile Thermostat is completely independently configured. Choose this option if you do not want to have any central configuration or management.
@@ -286,29 +202,19 @@ This component named __Versatile thermostat__ manage the following use cases :
> 3. In addition to this centralized configuration, all VTherms can be controlled by a single entity of type `select`. This function is named `central_mode`. This allows you to stop / start / freeze / etc. all VTherms at once. For each VTherm, the user indicates whether he is affected by this `central_mode`. > 3. In addition to this centralized configuration, all VTherms can be controlled by a single entity of type `select`. This function is named `central_mode`. This allows you to stop / start / freeze / etc. all VTherms at once. For each VTherm, the user indicates whether he is affected by this `central_mode`.
<details>
<summary>Creation of a new Versatile Thermostat</summary>
## Creation of a new Versatile Thermostat ## Creation of a new Versatile Thermostat
Click on Add integration button in the integration page Click on Add integration button in the integration page
![image](images/add-an-integration.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/add-an-integration.png?raw=true)
The configuration can be change through the same interface. Simply select the thermostat to change, hit "Configure" and you will be able to change some parameters or configuration. The configuration can be change through the same interface. Simply select the thermostat to change, hit "Configure" and you will be able to change some parameters or configuration.
Then choose the type of VTherm you want to create: Then follow the configurations steps as follow:
![image](images/config-main0.png)
</details>
<details>
<summary>Minimal configuration update</summary>
## Minimal configuration update ## Minimal configuration update
Then choose the “Main attributes” menu. ![image](/images/config-main0.png?raw=true)
![image](images/config-main.png) ![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)
@@ -321,19 +227,14 @@ Give the main mandatory attributes:
9. the possibility of controlling the thermostat centrally. Cf [centralized control](#centralized-control), 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. 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](images/tips.png) _*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**,
> 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily. > 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
</details>
<details>
<summary>Select the driven entity</summary>
## Select the driven entity ## Select the driven entity
Depending on your choice of thermostat type, you will need to choose one or more `switch`, `climate` or `number` type entities. Only entities compatible with the type are presented. Depending on your choice of thermostat type, you will need to choose one or more `switch`, `climate` or `number` type entities. Only entities compatible with the type are presented.
> ![Tip](images/tips.png) _*How to choose the type*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*How to choose the type*_
> The choice of type is important. Even if it is always possible to modify it afterwards via the configuration HMI, it is preferable to ask yourself the following few questions: > The choice of type is important. Even if it is always possible to modify it afterwards via the configuration HMI, it is preferable to ask yourself the following few questions:
> 1. **what type of equipment am I going to pilot?** In order, here is what to do: > 1. **what type of equipment am I going to pilot?** In order, here is what to do:
> 1. if you have a thermostatic valve (TRV) that can be controlled in Home Assistant via a ```number``` type entity (for example a _Shelly TRV_), choose the `over_valve` type. It is the most direct type and which ensures the best regulation, > 1. if you have a thermostatic valve (TRV) that can be controlled in Home Assistant via a ```number``` type entity (for example a _Shelly TRV_), choose the `over_valve` type. It is the most direct type and which ensures the best regulation,
@@ -343,21 +244,18 @@ Depending on your choice of thermostat type, you will need to choose one or more
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
### For a ```thermostat_over_switch``` type thermostat ### For a ```thermostat_over_switch``` type thermostat
![image](images/en/config-linked-entity.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
Some heater switches require regular "keep-alive messages" to prevent them from triggering a failsafe switch off. This feature can be enabled through the switch keep-alive interval configuration field.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm). The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
If several type entities are configured, the thermostat shifts the activations in order to minimize the number of switches active at a time t. This allows for better power distribution since each radiator will turn on in turn. If several type entities are configured, the thermostat shifts the activations in order to minimize the number of switches active at a time t. This allows for better power distribution since each radiator will turn on in turn.
Example of synchronized triggering: Example of synchronized triggering:
![image](images/multi-switch-activation.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on. If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on.
### For a thermostat of type ```thermostat_over_climate```: ### For a thermostat of type ```thermostat_over_climate```:
![image](images/config-linked-entity2.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling. It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling.
@@ -380,7 +278,7 @@ The self-regulation function is configured with:
These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often. These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often.
> ![Tip](images/tips.png) _*Implementation tip*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Implementation tip*_
> 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation, > 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation,
> 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved, > 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved,
> 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization, > 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization,
@@ -465,36 +363,6 @@ and of course, configure the VTherm's self-regulation mode in **Expert** mode. A
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat). For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
#### Internal temperature compensation
Sometimes, it happens that the internal thermometer of the underlying (TRV, air conditioning, etc.) is so wrong that self-regulation is not enough to regulate.
This happens when the internal thermometer is too close to the heat source. The internal temperature then rises much faster than the room temperature, which generates faults in the regulation.
Example :
1. the room temperature is 18°, the setpoint is 20°,
2. the internal temperature of the equipment is 22°,
3. if VTherm sends 21° as setpoint (= 20° + 1° auto-regulation), then the equipment will not heat because its internal temperature (22°) is above the setpoint (21°)
To overcome this, a new optional option was added in version 5.4: ![Use of internal temperature](images/config-use-internal-temp.png)
When enabled, this function will add the difference between the internal temperature and the room temperature to the setpoint to force heating.
In the example above, the difference is +4° (22° - 18°), so VTherm will send 25° (21°+4°) to the equipment forcing it to heat up.
This difference is calculated for each underlying because each has its own internal temperature. Think of a VTherm which would be connected to 3 TRVs each with its internal temperature for example.
We then obtain much more effective self-regulation which avoids the pitfall of large variations in faulty internal temperature.
#### synthesis of the self-regulation algorithm
The self-regulation algorithm can be summarized as follows:
1. initialize the target temperature as the VTherm setpoint,
1. If self-regulation is activated,
1. calculates the regulated temperature (valid for a VTherm),
2. take this temperature as a target,
2. For each underlying of the VTherm,
1. If "use internal temperature" is checked,
1. calculates the offset (trv internal temp - room temp),
2. Adding the offset to the target temperature,
3. sends the target temperature (= regulated temp + (internal temp - room temp)) to the underlying
#### Auto-fan mode #### Auto-fan mode
This mode introduced in 4.3 makes it possible to force the use of ventilation if the temperature difference is significant. In fact, by activating ventilation, distribution occurs more quickly, which saves time in reaching the target temperature. This mode introduced in 4.3 makes it possible to force the use of ventilation if the temperature difference is significant. In fact, by activating ventilation, distribution occurs more quickly, which saves time in reaching the target temperature.
You can choose which ventilation you want to activate between the following settings: Low, Medium, High, Turbo. You can choose which ventilation you want to activate between the following settings: Low, Medium, High, Turbo.
@@ -504,30 +372,21 @@ If your equipment does not include Turbo mode, Forte` mode will be used as a rep
Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen. Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen.
### For a thermostat of type ```thermostat_over_valve```: ### For a thermostat of type ```thermostat_over_valve```:
![image](images/config-linked-entity3.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves. You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm). The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
</details>
<details>
<summary>Configure the TPI algorithm coefficients</summary>
## Configure the TPI algorithm coefficients ## Configure the TPI algorithm coefficients
click on 'Validate' on the previous page, and if you choose a ```over_switch``` or ```over_valve``` thermostat and you will get there:
Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the "TPI" menu option, you will get there: ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
![image](images/config-tpi.png)
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm). For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
</details>
<details>
<summary>Configure the preset temperature</summary>
## Configure the preset temperature ## Configure the preset temperature
Click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presets.png?raw=true)
The preset mode allows you to pre-configurate targeted temperature. Used in conjonction with Scheduler (see [scheduler](#even-better-with-scheduler-component) you will have a powerfull and simple way to optimize the temperature vs electrical consumption of your hous. Preset handled are the following : The preset mode allows you to pre-configurate targeted temperature. Used in conjonction with Scheduler (see [scheduler](#even-better-with-scheduler-component) you will have a powerfull and simple way to optimize the temperature vs electrical consumption of your hous. Preset handled are the following :
- **Eco** : device is running an energy-saving mode - **Eco** : device is running an energy-saving mode
@@ -538,22 +397,14 @@ The preset mode allows you to pre-configurate targeted temperature. Used in conj
**None** is always added in the list of modes, as it is a way to not use the presets modes but a **manual temperature** instead. **None** is always added in the list of modes, as it is a way to not use the presets modes but a **manual temperature** instead.
The pre-settings are made (since v6.0) directly from the VTherm entities or from the central configuration if you use the central configuration. > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> ![Tip](images/tips.png) _*Notes*_
> 1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available. > 1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
> 2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management). > 2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management).
> 3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management). > 3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management).
> 4. if you uses the advanced configuration you will see the preset set to ``safety`` if the temperature could not be retrieved after a certain delay > 4. if you uses the advanced configuration you will see the preset set to ``safety`` if the temperature could not be retrieved after a certain delay
> 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component > 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
</details>
<details>
<summary>Configure the doors/windows turning on/off the thermostats</summary>
## Configure the doors/windows turning on/off the thermostats ## Configure the doors/windows turning on/off the thermostats
You must have chosen the ```With opening detection``` feature on the first page to arrive on this page. You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
The detection of openings can be done in 2 ways: The detection of openings can be done in 2 ways:
1. either with a sensor placed on the opening (sensor mode), 1. either with a sensor placed on the opening (sensor mode),
@@ -561,14 +412,14 @@ The detection of openings can be done in 2 ways:
### The sensor mode ### The sensor mode
In sensor mode, you must fill in the following information: In sensor mode, you must fill in the following information:
![image](images/config-window-sensor.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-sensor.png?raw=true)
1. an entity ID of a **window/door sensor**. It should be a binary_sensor or an input_boolean. The state of the entity must be 'on' when the window is open or 'off' when it is closed 1. an entity ID of a **window/door sensor**. It should be a binary_sensor or an input_boolean. The state of the entity must be 'on' when the window is open or 'off' when it is closed
2. a **delay in seconds** before any change. This allows a window to be opened quickly without stopping the heating. 2. a **delay in seconds** before any change. This allows a window to be opened quickly without stopping the heating.
### Auto mode ### Auto mode
In auto mode, the configuration is as follows: In auto mode, the configuration is as follows:
![image](images/config-window-auto.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-auto.png?raw=true)
1. a detection threshold in degrees per minute. When the temperature drops below this threshold, the thermostat will turn off. The lower this value, the faster the detection will be (in return for a risk of false positives), 1. a detection threshold in degrees per minute. When the temperature drops below this threshold, the thermostat will turn off. The lower this value, the faster the detection will be (in return for a risk of false positives),
2. an end of detection threshold in degrees per minute. When the temperature drop goes above this value, the thermostat will go back to the previous mode (mode and preset), 2. an end of detection threshold in degrees per minute. When the temperature drop goes above this value, the thermostat will go back to the previous mode (mode and preset),
@@ -580,27 +431,22 @@ To set the thresholds it is advisable to start with the reference values a
- maximum duration: 60 min. - maximum duration: 60 min.
A new "slope" sensor has been added for all thermostats. It gives the slope of the temperature curve in °C/min (or °K/min). This slope is smoothed and filtered to avoid aberrant values from the thermometers which would interfere with the measurement. A new "slope" sensor has been added for all thermostats. It gives the slope of the temperature curve in °C/min (or °K/min). This slope is smoothed and filtered to avoid aberrant values from the thermometers which would interfere with the measurement.
![image](images/temperature-slope.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/temperature-slope.png?raw=true)
To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"): To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"):
![image](images/window-auto-tuning.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/window-auto-tuning.png?raw=true)
And that's all ! your thermostat will turn off when the windows are open and turn back on when closed. And that's all ! your thermostat will turn off when the windows are open and turn back on when closed.
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. If you want to use **multiple door/window sensors** to automate your thermostat, just create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/) > 1. If you want to use **multiple door/window sensors** to automate your thermostat, just create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
> 2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank, > 2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank,
> 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time, > 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
> 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...) > 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
</details>
<details>
<summary>Configure the activity mode or motion detection</summary>
## Configure the activity mode or motion detection ## Configure the activity mode or motion detection
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there: If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
![image](images/config-motion.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true)
We will now see how to configure the new Activity mode. We will now see how to configure the new Activity mode.
What we need: What we need:
@@ -619,18 +465,13 @@ What we need:
For this to work, the climate thermostat should be in ``Activity`` preset mode. For this to work, the climate thermostat should be in ``Activity`` preset mode.
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface > 1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
</details>
<details>
<summary>Configure the power management</summary>
## Configure the power management ## Configure the power management
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there: If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
![image](images/config-power.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true)
This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heaters linked to the VTherm** (in the first step of the configuration) and the algorithm will not start a radiator if the max power will be exceeded after radiator starts. This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heaters linked to the VTherm** (in the first step of the configuration) and the algorithm will not start a radiator if the max power will be exceeded after radiator starts.
@@ -638,23 +479,18 @@ This feature allows you to regulate the power consumption of your radiators. Kno
Note that all power values should have the same units (kW or W for example). Note that all power values should have the same units (kW or W for example).
This allows you to change the max power along time using a Scheduler or whatever you like. This allows you to change the max power along time using a Scheduler or whatever you like.
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually. > 1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually.
> 2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation. > 2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
> 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement. > 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
> 4. If you don't want to use this feature, just leave the entities id empty > 4. If you don't want to use this feature, just leave the entities id empty
> 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power. > 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power.
</details>
<details>
<summary>Configure presence or occupancy</summary>
## Configure presence or occupancy ## Configure presence or occupancy
If selected on the first page, this feature allows you to dynamically change the temperature of all configured thermostat presets when no one is home or when someone comes home. To do this, you must configure the temperature that will be used for each preset when presence is disabled. When the presence sensor turns off, these temperatures will be used. When it turns back on, the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature). If selected on the first page, this feature allows you to dynamically change the temperature of all configured thermostat presets when no one is home or when someone comes home. To do this, you must configure the temperature that will be used for each preset when presence is disabled. When the presence sensor turns off, these temperatures will be used. When it turns back on, the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature).
To configure presence, complete this form: To configure presence, complete this form:
![image](images/config-presence.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true)
To do this, you must configure: To do this, you must configure:
1. An **occupancy sensor** whose state must be 'on' or 'home' if someone is present or 'off' or 'not_home' otherwise, 1. An **occupancy sensor** whose state must be 'on' or 'home' if someone is present or 'off' or 'not_home' otherwise,
@@ -666,21 +502,15 @@ If AC mode is used, you will also be able to configure temperatures when the equ
ATTENTION: groups of people do not function as a presence sensor. They are not recognized as a presence sensor. You must use a template as described here [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor). ATTENTION: groups of people do not function as a presence sensor. They are not recognized as a presence sensor. You must use a template as described here [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor).
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. the change in temperature is immediate and is reflected on the front shutter. The calculation will take into account the new target temperature at the next calculation of the cycle, > 1. the change in temperature is immediate and is reflected on the front shutter. The calculation will take into account the new target temperature at the next calculation of the cycle,
> 2. you can use the person.xxxx direct sensor or a group of Home Assistant sensors. The presence sensor manages the ``on`` or ``home`` states as present and the ``off`` or ``not_home`` states as absent. > 2. you can use the person.xxxx direct sensor or a group of Home Assistant sensors. The presence sensor manages the ``on`` or ``home`` states as present and the ``off`` or ``not_home`` states as absent.
</details>
<details>
<summary>Advanced configuration</summary>
## Advanced configuration ## Advanced configuration
Those parameters allows to fine tune the thermostat. Those parameters allows to fine tune the thermostat.
The advanced configuration form is the following: The advanced configuration form is the following:
![image](images/config-advanced.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-advanced.png?raw=true)
The first delay (minimal_activation_delay_sec) in sec in the minimum delay acceptable for turning on the heater. When calculation gives a power on delay below this value, the heater will stays off. The first delay (minimal_activation_delay_sec) in sec in the minimum delay acceptable for turning on the heater. When calculation gives a power on delay below this value, the heater will stays off.
@@ -702,20 +532,14 @@ By default, the outdoor thermometer can trigger a trip if it no longer sends a v
See [example tuning](#examples-tuning) for common tuning examples See [example tuning](#examples-tuning) for common tuning examples
>![Tip](images/tips.png) _*Notes*_ >![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value, > 1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
> 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "safety" preset, > 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "safety" preset,
> 3. A service is available that allows you to set the 3 safety parameters. This can be used to adapt the safety function to your use. > 3. A service is available that allows you to set the 3 safety parameters. This can be used to adapt the safety function to your use.
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``, > 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature. > 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature.
</details>
<details>
<summary>Centralized control</summary>
## Centralized control ## Centralized control
Since release 5.2, if you have defined a centralized configuration, you have a new entity named `select.central_mode` which allows you to control all VTherms with a single action. For a VTherm to be centrally controllable, its configuration attribute named `use_central_mode` must be true. Since release 5.2, if you have defined a centralized configuration, you have a new entity named `select.central_mode` which allows you to control all VTherms with a single action. For a VTherm to be centrally controllable, its configuration attribute named `use_central_mode` must be true.
This entity is presented in the form of a list of choices which contains the following choices: This entity is presented in the form of a list of choices which contains the following choices:
@@ -728,15 +552,9 @@ This entity is presented in the form of a list of choices which contains the fol
It is therefore possible to control all VTherms (only those explicitly designated) with a single control. It is therefore possible to control all VTherms (only those explicitly designated) with a single control.
Example rendering: Example rendering:
![central_mode](images/central_mode.png) ![central_mode](/images/central_mode.png?raw=true)
</details>
<details>
<summary>Control of a central boiler</summary>
## Control of a central boiler ## Control of a central boiler
Since release 5.3, you have the possibility of controlling a centralized boiler. From the moment it is possible to start or stop this boiler from Home Assistant, then Versatile Thermostat will be able to control it directly. Since release 5.3, you have the possibility of controlling a centralized boiler. From the moment it is possible to start or stop this boiler from Home Assistant, then Versatile Thermostat will be able to control it directly.
The principle put in place is generally as follows: The principle put in place is generally as follows:
@@ -752,16 +570,16 @@ The principle put in place is generally as follows:
You therefore always have the information which allows you to control and adjust the activation of the boiler. You therefore always have the information which allows you to control and adjust the activation of the boiler.
All these entities are attached to the central configuration service: All these entities are attached to the central configuration service:
![The entities controlling the boiler](images/entitites-central-boiler.png) ![The entities controlling the boiler](/images/entitites-central-boiler.png?raw=true)
### Setup ### Setup
To configure this function, you must have a centralized configuration (see [Configuration](#configuration)) and check the 'Add a central boiler' box: To configure this function, you must have a centralized configuration (see [Configuration](#configuration)) and check the 'Add a central boiler' box:
![Adding a central boiler](images/config-central-boiler-1.png) ![Adding a central boiler](/images/config-central-boiler-1.png?raw=true)
On the following page you can configure the services to be called when switching the boiler on/off: On the following page you can configure the services to be called when switching the boiler on/off:
![Adding a central boiler](images/config-central-boiler-2.png) ![Adding a central boiler](/images/config-central-boiler-2.png?raw=true)
The services are configured as indicated on the page: The services are configured as indicated on the page:
1. the general format is `entity_id/service_id[/attribute:value]` (where `/attribute:value` is optional), 1. the general format is `entity_id/service_id[/attribute:value]` (where `/attribute:value` is optional),
@@ -784,11 +602,11 @@ Example:
Under "Development Tools / Service": Under "Development Tools / Service":
![Service configuration](images/dev-tools-turnon-boiler-1.png) ![Service configuration](/images/dev-tools-turnon-boiler-1.png?raw=true)
In yaml mode: In yaml mode:
![Service configuration](images/dev-tools-turnon-boiler-2.png) ![Service configuration](/images/dev-tools-turnon-boiler-2.png?raw=true)
The service to configure is then the following: `climate.empty_thermostast/climate.set_hvac_mode/hvac_mode:heat` (note the removal of the blank in `hvac_mode:heat`) The service to configure is then the following: `climate.empty_thermostast/climate.set_hvac_mode/hvac_mode:heat` (note the removal of the blank in `hvac_mode:heat`)
@@ -833,17 +651,12 @@ context:
### Warning ### Warning
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> Controlling a central boiler using software or hardware such as home automation can pose risks to its proper functioning. Before using these functions, make sure that your boiler has safety functions and that they are working. Turning on a boiler if all the taps are closed can generate excess pressure, for example. > Controlling a central boiler using software or hardware such as home automation can pose risks to its proper functioning. Before using these functions, make sure that your boiler has safety functions and that they are working. Turning on a boiler if all the taps are closed can generate excess pressure, for example.
</details> ## Parameters synthesis
<details> | Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "central configuration" |
<summary>Parameter summary</summary>
## Parameter summary
| Parameter | Description | "over switch" | "over climate" | "over valve" | "central configuration" |
| ----------------------------------------- | ----------------------------------------------------------------------------- | ------------- | ------------------- | ------------ | ----------------------- | | ----------------------------------------- | ----------------------------------------------------------------------------- | ------------- | ------------------- | ------------ | ----------------------- |
| ``name`` | Name | X | X | X | - | | ``name`` | Name | X | X | X | - |
| ``thermostat_type`` | Thermostat type | X | X | X | - | | ``thermostat_type`` | Thermostat type | X | X | X | - |
@@ -862,7 +675,6 @@ context:
| ``heater_entity2_id`` | 2nd heater switch | X | - | - | - | | ``heater_entity2_id`` | 2nd heater switch | X | - | - | - |
| ``heater_entity3_id`` | 3rd heater switch | X | - | - | - | | ``heater_entity3_id`` | 3rd heater switch | X | - | - | - |
| ``heater_entity4_id`` | 4th heater switch | X | - | - | - | | ``heater_entity4_id`` | 4th heater switch | X | - | - | - |
| ``heater_keep_alive`` | Switch keep-alive interval | X | - | - | - |
| ``proportional_function`` | Algorithm | X | - | X | - | | ``proportional_function`` | Algorithm | X | - | X | - |
| ``climate_entity1_id`` | 1rst underlying climate | - | X | - | - | | ``climate_entity1_id`` | 1rst underlying climate | - | X | - | - |
| ``climate_entity2_id`` | 2nd underlying climate | - | X | - | - | | ``climate_entity2_id`` | 2nd underlying climate | - | X | - | - |
@@ -875,6 +687,13 @@ context:
| ``ac_mode`` | Use the Air Conditioning (AC) mode | X | X | X | - | | ``ac_mode`` | Use the Air Conditioning (AC) mode | X | X | X | - |
| ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - | X | X | | ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - | X | X |
| ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - | X | X | | ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - | X | X |
| ``frost_temp`` | Temperature in frost protection preset | X | X | X | X |
| ``eco_temp`` | Temperature in Eco preset | X | X | X | X |
| ``comfort_temp`` | Temperature in Comfort preset | X | X | X | X |
| ``boost_temp`` | Temperature in Boost preset | X | X | X | X |
| ``eco_ac_temp`` | Temperature in Eco preset for AC mode | X | X | X | X |
| ``comfort_ac_temp`` | Temperature in Comfort preset for AC mode | X | X | X | X |
| ``boost_ac_temp`` | Temperature in Boost preset for AC mode | X | X | X | X |
| ``window_sensor_entity_id`` | Window sensor entity id | X | X | X | - | | ``window_sensor_entity_id`` | Window sensor entity id | X | X | X | - |
| ``window_delay`` | Window sensor delay (seconds) | X | X | X | X | | ``window_delay`` | Window sensor delay (seconds) | X | X | X | X |
| ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X | X | X | | ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X | X | X |
@@ -889,6 +708,13 @@ context:
| ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X | X | X | | ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X | X | X |
| ``power_temp`` | Temperature for Power shedding | X | X | X | X | | ``power_temp`` | Temperature for Power shedding | X | X | X | X |
| ``presence_sensor_entity_id`` | Presence sensor entity id | X | X | X | X | | ``presence_sensor_entity_id`` | Presence sensor entity id | X | X | X | X |
| ``frost_away_temp`` | Temperature in Frost protection preset when no presence | X | X | X | X |
| ``eco_away_temp`` | Temperature in Eco preset when no presence | X | X | X | X |
| ``comfort_away_temp`` | Temperature in Comfort preset when no presence | X | X | X | X |
| ``boost_away_temp`` | Temperature in Boost preset when no presence | X | X | X | X |
| ``eco_ac_away_temp`` | Temperature in Eco preset when no presence in AC mode | X | X | X | X |
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X | X | X |
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X | X | X |
| ``minimal_activation_delay`` | Minimal activation delay | X | - | X | X | | ``minimal_activation_delay`` | Minimal activation delay | X | - | X | X |
| ``security_delay_min`` | Safety delay (in minutes) | X | - | X | X | | ``security_delay_min`` | Safety delay (in minutes) | X | - | X | X |
| ``security_min_on_percent`` | Minimal power percent to enable safety mode | X | - | X | X | | ``security_min_on_percent`` | Minimal power percent to enable safety mode | X | - | X | X |
@@ -898,14 +724,13 @@ context:
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - | | ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
| ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - | - | | ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - | - |
| ``auto_fan_mode`` | Auto fan mode | - | X | - | - | | ``auto_fan_mode`` | Auto fan mode | - | X | - | - |
| ``auto_regulation_use_device_temp`` | Use the internal temperature of the underlying device | - | X | - | - | | ``add_central_boiler_control`` | Add the control of a central boiler | - | - | - | X |
| ``use_central_boiler_feature`` | Add the control of a central boiler | - | - | - | X |
| ``central_boiler_activation_service`` | Activation service of the boiler | - | - | - | X | | ``central_boiler_activation_service`` | Activation service of the boiler | - | - | - | X |
| ``central_boiler_deactivation_service`` | Deactivaiton service of the boiler | - | - | - | X | | ``central_boiler_deactivation_service`` | Deactivaiton service of the boiler | - | - | - | X |
| ``used_by_controls_central_boiler`` | Indicate if the VTherm control the central boiler | X | X | X | - | | ``used_by_controls_central_boiler`` | Indicate if the VTherm control the central boiler | X | X | X | - |
</details>
# Tuning examples
# Examples tuning
## Electrical heater ## Electrical heater
- cycle: between 5 and 10 minutes, - cycle: between 5 and 10 minutes,
@@ -979,7 +804,7 @@ See some situations at [examples](#some-results).
With the thermostat are available sensors that allow you to view the alerts and the internal status of the thermostat. They are available in the entities of the device associated with the thermostat: With the thermostat are available sensors that allow you to view the alerts and the internal status of the thermostat. They are available in the entities of the device associated with the thermostat:
![image](images/thermostat-sensors.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/thermostat-sensors.png?raw=true)
In order, there are: In order, there are:
1. the main climate thermostat command entity, 1. the main climate thermostat command entity,
@@ -1012,7 +837,7 @@ frontend:
``` ```
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this: and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
![image](images/colored-thermostat-sensors.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/colored-thermostat-sensors.png?raw=true)
# Services # Services
@@ -1058,7 +883,7 @@ target:
entity_id: climate.my_thermostat entity_id: climate.my_thermostat
``` ```
> ![Tip](images/tips.png) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration. - after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration.
## Change safety settings ## Change safety settings
@@ -1111,7 +936,7 @@ You can very easily capture its events in an automation, for example to notify u
# Custom attributes # Custom attributes
To tune the algorithm you have access to all context seen and calculted by the thermostat through dedicated attributes. You can see (and use) those attributes in the "Development tools / states" HMI of HA. Enter your thermostat and you will see something like this: To tune the algorithm you have access to all context seen and calculted by the thermostat through dedicated attributes. You can see (and use) those attributes in the "Development tools / states" HMI of HA. Enter your thermostat and you will see something like this:
![image](images/dev-tools-climate.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/dev-tools-climate.png?raw=true)
Custom attributes are the following: Custom attributes are the following:
@@ -1162,23 +987,23 @@ Custom attributes are the following:
# Some results # Some results
**Convergence of temperature to target configured by preset:** **Convergence of temperature to target configured by preset:**
![image](images/results-1.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-1.png?raw=true)
[Cycle of on/off calculated by the integration:](https://) [Cycle of on/off calculated by the integration:](https://)
![image](images/results-2.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-2.png?raw=true)
**Coef_int too high (oscillations around the target)** **Coef_int too high (oscillations around the target)**
![image](images/results-3.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-3.png?raw=true)
**Algorithm calculation evolution** **Algorithm calculation evolution**
![image](images/results-4.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-4.png?raw=true)
See the code of this component [[below](#even-better-with-apex-chart-to-tune-your-thermostat)] See the code of this component [[below](#even-better-with-apex-chart-to-tune-your-thermostat)]
**Fine tuned thermostat** **Fine tuned thermostat**
Thank's [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) ! Thank's [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) !
We can see stability around the target temperature (consigne) and when at target the on_percent (puissance) is near 0.3 which seems a very good value. We can see stability around the target temperature (consigne) and when at target the on_percent (puissance) is near 0.3 which seems a very good value.
![image](images/results-fine-tuned.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-fine-tuned.png?raw=true)
Enjoy ! Enjoy !
@@ -1217,7 +1042,7 @@ I hope this example helps you, don't hesitate to give me your feedbacks !
## Even-even better with custom:simple-thermostat front integration ## Even-even better with custom:simple-thermostat front integration
The ``custom:simple-thermostat`` [here](https://github.com/nervetattoo/simple-thermostat) is a great integration which allow some customisation which fits well with this thermostat. The ``custom:simple-thermostat`` [here](https://github.com/nervetattoo/simple-thermostat) is a great integration which allow some customisation which fits well with this thermostat.
You can have something like that very easily ![image](images/simple-thermostat.png) You can have something like that very easily ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/simple-thermostat.png?raw=true)
Example configuration: Example configuration:
``` ```
@@ -1259,7 +1084,7 @@ You can customize this component using the HACS card-mod component to adjust the
} }
{% endif %} {% endif %}
``` ```
![image](images/custom-css-thermostat.png) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true)
## Even better with Plotly to tune your Thermostat ## Even better with Plotly to tune your Thermostat
You can get curve like presented in [some results](#some-results) with kind of Plotly configuration only using the custom attributes of the thermostat described [here](#custom-attributes): You can get curve like presented in [some results](#some-results) with kind of Plotly configuration only using the custom attributes of the thermostat described [here](#custom-attributes):
@@ -1332,7 +1157,7 @@ Replace values in [[ ]] by yours.
Example of graph obtained with Plotly : Example of graph obtained with Plotly :
![image](images/plotly-curves.png) ![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
@@ -1428,11 +1253,7 @@ If you want to contribute to this please read the [Contribution guidelines](CONT
# Troubleshooting # Troubleshooting
<details>
<summary>Using a Heatzy</summary>
## Using a Heatzy ## Using a Heatzy
The use of a Heatzy is possible provided you use a virtual switch on this model: The use of a Heatzy is possible provided you use a virtual switch on this model:
``` ```
- platform:template - platform:template
@@ -1462,11 +1283,6 @@ The use of a Heatzy is possible provided you use a virtual switch on this model:
``` ```
Thanks to @gael for this example. Thanks to @gael for this example.
</details>
<details>
<summary>Using a Heatsink with a Pilot Wire</summary>
## Using a Heatsink with a Pilot Wire ## Using a Heatsink with a Pilot Wire
As with the Heatzy above you can use a virtual switch which will change the preset of your radiator depending on the ignition state of the VTherm. As with the Heatzy above you can use a virtual switch which will change the preset of your radiator depending on the ignition state of the VTherm.
Example : Example :
@@ -1486,19 +1302,10 @@ Example :
entity_id: switch.radiateur_soan entity_id: switch.radiateur_soan
icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}" icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}"
``` ```
</details>
<details>
<summary>Only the first radiator heats</summary>
## Only the first radiator heats ## Only the first radiator heats
In `over_switch` mode if several radiators are configured for the same VTherm, switching on will be done sequentially to smooth out consumption peaks as much as possible. In `over_switch` mode if several radiators are configured for the same VTherm, switching on will be done sequentially to smooth out consumption peaks as much as possible.
This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch)v This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch)v
</details>
<details>
<summary>The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint</summary>
## The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint ## The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint
@@ -1511,10 +1318,6 @@ With an `over_climate` type VTherm, the regulation is done by the underlying `cl
Example of discussion around these topics: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312 ), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278) Example of discussion around these topics: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312 ), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278)
To get around this, VTherm is equipped with a function called self-regulation which allows the instruction sent to the underlying to be adapted until the target temperature is respected. This function compensates for the measurement bias of internal thermometers. If the bias is important the regulation must be important. See [Self-regulation](#self-regulation) to configure self-regulation. To get around this, VTherm is equipped with a function called self-regulation which allows the instruction sent to the underlying to be adapted until the target temperature is respected. This function compensates for the measurement bias of internal thermometers. If the bias is important the regulation must be important. See [Self-regulation](#self-regulation) to configure self-regulation.
</details>
<details>
<summary>Adjust window opening detection parameters in auto mode</summary>
## Adjust window opening detection parameters in auto mode ## Adjust window opening detection parameters in auto mode
@@ -1535,13 +1338,8 @@ versatile_thermostat:
``` ```
These parameters are sensitive and quite difficult to adjust. Please only use them if you know what you are doing and your temperature measurements are not already smooth. These parameters are sensitive and quite difficult to adjust. Please only use them if you know what you are doing and your temperature measurements are not already smooth.
</details>
<details>
<summary>Why does my Versatile Thermostat go into Safety?</summary>
## Why does my Versatile Thermostat go into Safety? ## Why does my Versatile Thermostat go into Safety?
Safety mode is only possible on VTherm `over_switch` and `over_valve`. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`. Safety mode is only possible on VTherm `over_switch` and `over_valve`. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`.
As the algorithm is based on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To avoid this, when the conditions mentioned above are detected, heating is limited to the `security_default_on_percent` parameter. This value must therefore be reasonably low. It helps prevent a fire while avoiding completely cutting off the radiator (risk of freezing). As the algorithm is based on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To avoid this, when the conditions mentioned above are detected, heating is limited to the `security_default_on_percent` parameter. This value must therefore be reasonably low. It helps prevent a fire while avoiding completely cutting off the radiator (risk of freezing).
@@ -1552,11 +1350,11 @@ All these parameters are adjusted on the last page of the VTherm configuration:
The first symptom is an abnormally low temperature with a slow and regular heating time in each cycle. The first symptom is an abnormally low temperature with a slow and regular heating time in each cycle.
Example: Example:
[safety mode](images/security-mode-symptome1.png) [safety mode](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome1.png?raw=true)
If you installed the [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), the VTherm in question will have this shape: If you installed the [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), the VTherm in question will have this shape:
[safety mode UI Card](images/security-mode-symptome2.png) [safety mode UI Card](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome2.png?raw=true)
You can also check in the VTherm attributes the dates of receipt of the different dates. Attributes are available in Development Tools / Reports. You can also check in the VTherm attributes the dates of receipt of the different dates. Attributes are available in Development Tools / Reports.
@@ -1587,13 +1385,8 @@ This will depend on the cause of the problem:
2. If the `security_delay_min` parameter is too small, it risks generating a lot of false alerts. A correct value is around 60 min, especially if you have battery-powered temperature sensors. 2. If the `security_delay_min` parameter is too small, it risks generating a lot of false alerts. A correct value is around 60 min, especially if you have battery-powered temperature sensors.
3. Some temperature sensors do not send a measurement if the temperature has not changed. So in the event of a very stable temperature for a long time, the safety mode may be triggered. This is not very serious since it is removed as soon as the VTherm receives a temperature again. On certain thermometers (TuYA for example), you can force the maximum delay between 2 measurements. It will be appropriate to set a max delay < `security_delay_min`, 3. Some temperature sensors do not send a measurement if the temperature has not changed. So in the event of a very stable temperature for a long time, the safety mode may be triggered. This is not very serious since it is removed as soon as the VTherm receives a temperature again. On certain thermometers (TuYA for example), you can force the maximum delay between 2 measurements. It will be appropriate to set a max delay < `security_delay_min`,
4. As soon as the temperature is received again the safety mode will be removed and the previous values of preset, target temperature and mode will be restored. 4. As soon as the temperature is received again the safety mode will be removed and the previous values of preset, target temperature and mode will be restored.
</details>
<details>
<summary>Using a group of people as a presence sensor</summary>
## Using a group of people as a presence sensor ## Using a group of people as a presence sensor
Unfortunately, groups of people are not recognized as presence sensors. We cannot therefore use them directly in VTherm. Unfortunately, groups of people are not recognized as presence sensors. We cannot therefore use them directly in VTherm.
The workaround is to create a binary_sensor template with the following code: The workaround is to create a binary_sensor template with the following code:
@@ -1614,21 +1407,6 @@ You will note in this example, the use of an input_boolean named force_presence
template: !include templates.yaml template: !include templates.yaml
... ...
``` ```
</details>
<details>
<summary>Enable Versatile Thermostat logs</summary>
## Enable Versatile Thermostat logs
Sometimes you will need to enable logs to refine the analyses. To do this, edit the `logger.yaml` file of your configuration and configure the logs as follows:
```
default: xxxx
logs:
custom_components.versatile_thermostat: info
```
You must reload the yaml configuration (Dev Tools / Yaml / All Yaml configuration) or restart Home Assistant for this change to take effect.
</details>
*** ***

View File

@@ -1,5 +1,4 @@
"""The Versatile Thermostat integration.""" """The Versatile Thermostat integration."""
from __future__ import annotations from __future__ import annotations
from typing import Dict from typing import Dict
@@ -9,18 +8,16 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED from homeassistant.const import SERVICE_RELOAD
from homeassistant.config_entries import ConfigEntry, ConfigType from homeassistant.config_entries import ConfigEntry, ConfigType
from homeassistant.core import HomeAssistant, CoreState, callback from homeassistant.core import HomeAssistant
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .const import ( from .const import (
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
CONFIG_VERSION,
CONFIG_MINOR_VERSION,
CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_STRONG,
@@ -30,13 +27,6 @@ from .const import (
CONF_SAFETY_MODE, CONF_SAFETY_MODE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_POWER_SENSOR,
CONF_PRESENCE_SENSOR,
) )
from .vtherm_api import VersatileThermostatAPI from .vtherm_api import VersatileThermostatAPI
@@ -92,27 +82,15 @@ async def async_setup(
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# L'argument config contient votre fichier configuration.yaml # L'argument config contient votre fichier configuration.yaml
vtherm_config = config.get(DOMAIN) vtherm_config = config.get(DOMAIN)
if vtherm_config is not None: if vtherm_config is not None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
api.set_global_config(vtherm_config) api.set_global_config(vtherm_config)
else: else:
_LOGGER.info("No global config from configuration.yaml available") _LOGGER.info("No global config from configuration.yaml available")
# Listen HA starts to initialize all links between
@callback
async def _async_startup_internal(*_):
_LOGGER.info(
"VersatileThermostat - HA is started, initialize all links between VTherm entities"
)
await api.init_vtherm_links()
if hass.state == CoreState.running:
await _async_startup_internal()
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_startup_internal)
hass.helpers.service.async_register_admin_service( hass.helpers.service.async_register_admin_service(
DOMAIN, DOMAIN,
SERVICE_RELOAD, SERVICE_RELOAD,
@@ -136,7 +114,6 @@ async def reload_all_vtherm(hass):
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api: if api:
await api.reload_central_boiler_entities_list() await api.reload_central_boiler_entities_list()
await api.init_vtherm_links()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -157,7 +134,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await api.reload_central_boiler_entities_list() await api.reload_central_boiler_entities_list()
await api.init_vtherm_links()
return True return True
@@ -172,7 +148,6 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api is not None: if api is not None:
await api.reload_central_boiler_entities_list() await api.reload_central_boiler_entities_list()
await api.init_vtherm_links()
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -190,40 +165,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Example migration function # Example migration function
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry.""" """Migrate old entry."""
_LOGGER.debug( _LOGGER.debug("Migrating from version %s", config_entry.version)
"Migrating from version %s/%s", config_entry.version, config_entry.minor_version
)
if ( if config_entry.version == 1:
config_entry.version != CONFIG_VERSION
or config_entry.minor_version != CONFIG_MINOR_VERSION
):
_LOGGER.debug(
"Migration to %s/%s is needed", CONFIG_VERSION, CONFIG_MINOR_VERSION
)
new = {**config_entry.data} new = {**config_entry.data}
# TO DO: modify Config Entry data if there will be something to migrate
if ( config_entry.version = 2
config_entry.data.get(CONF_THERMOSTAT_TYPE) hass.config_entries.async_update_entry(config_entry, data=new)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
new[CONF_USE_WINDOW_FEATURE] = True
new[CONF_USE_MOTION_FEATURE] = True
new[CONF_USE_POWER_FEATURE] = new.get(CONF_POWER_SENSOR, None) is not None
new[CONF_USE_PRESENCE_FEATURE] = (
new.get(CONF_PRESENCE_SENSOR, None) is not None
)
new[CONF_USE_CENTRAL_BOILER_FEATURE] = new.get( _LOGGER.info("Migration to version %s successful", config_entry.version)
"add_central_boiler_control", False
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
hass.config_entries.async_update_entry(
config_entry,
data=new,
version=CONFIG_VERSION,
minor_version=CONFIG_MINOR_VERSION,
)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True return True

View File

@@ -82,9 +82,9 @@ from .const import (
CONF_NO_MOTION_PRESET, CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER, CONF_DEVICE_POWER,
CONF_PRESETS, CONF_PRESETS,
# CONF_PRESETS_AWAY, CONF_PRESETS_AWAY,
# CONF_PRESETS_WITH_AC, CONF_PRESETS_WITH_AC,
# CONF_PRESETS_AWAY_WITH_AC, CONF_PRESETS_AWAY_WITH_AC,
CONF_CYCLE_MIN, CONF_CYCLE_MIN,
CONF_PROP_FUNCTION, CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT, CONF_TPI_COEF_INT,
@@ -111,7 +111,6 @@ from .const import (
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_PRESENCE_FEATURE,
CONF_TEMP_MAX, CONF_TEMP_MAX,
CONF_TEMP_MIN, CONF_TEMP_MIN,
HIDDEN_PRESETS, HIDDEN_PRESETS,
@@ -214,11 +213,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
super().__init__() super().__init__()
# To remove some silly warning event if code is fixed
self._enable_turn_on_off_backwards_compatibility = False
self._hass = hass self._hass = hass
self._entry_infos = None
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
self._unique_id = unique_id self._unique_id = unique_id
@@ -290,15 +285,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._last_central_mode = None self._last_central_mode = None
self._is_used_by_central_boiler = False self._is_used_by_central_boiler = False
self._support_flags = None
# Preset will be initialized from Number entities
self._presets: dict[str, Any] = {} # presets
self._presets_away: dict[str, Any] = {} # presets_away
self._attr_preset_modes: list[str] | None
self._use_central_config_temperature = False
self.post_init(entry_infos) self.post_init(entry_infos)
def clean_central_config_doublon( def clean_central_config_doublon(
@@ -321,6 +307,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if cfg.get(CONF_USE_TPI_CENTRAL_CONFIG) is True: if cfg.get(CONF_USE_TPI_CENTRAL_CONFIG) is True:
clean_one(cfg, STEP_CENTRAL_TPI_DATA_SCHEMA) clean_one(cfg, STEP_CENTRAL_TPI_DATA_SCHEMA)
if cfg.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is True:
clean_one(cfg, STEP_CENTRAL_PRESETS_DATA_SCHEMA)
clean_one(cfg, STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA)
if cfg.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is True: if cfg.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is True:
clean_one(cfg, STEP_CENTRAL_WINDOW_DATA_SCHEMA) clean_one(cfg, STEP_CENTRAL_WINDOW_DATA_SCHEMA)
@@ -361,22 +351,40 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.info("%s - The merged configuration is %s", self, entry_infos) _LOGGER.info("%s - The merged configuration is %s", self, entry_infos)
self._entry_infos = entry_infos
self._use_central_config_temperature = entry_infos.get(
CONF_USE_PRESETS_CENTRAL_CONFIG
) or (
entry_infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
and entry_infos.get(CONF_USE_PRESENCE_FEATURE)
)
self._ac_mode = entry_infos.get(CONF_AC_MODE) is True self._ac_mode = entry_infos.get(CONF_AC_MODE) is True
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX) self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN) self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
if (step := entry_infos.get(CONF_STEP_TEMPERATURE)) is not None: if (step := entry_infos.get(CONF_STEP_TEMPERATURE)) is not None:
self._attr_target_temperature_step = step self._attr_target_temperature_step = step
self._attr_preset_modes: list[str] | None # convert entry_infos into usable attributes
presets: dict[str, Any] = {}
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos:
presets[key] = entry_infos.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
presets[key] = (
self._attr_max_temp if self._ac_mode else self._attr_min_temp
)
presets_away: dict[str, Any] = {}
items = (
CONF_PRESETS_AWAY_WITH_AC.items()
if self._ac_mode
else CONF_PRESETS_AWAY.items()
)
for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos:
presets_away[key] = entry_infos.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
presets_away[key] = (
self._attr_max_temp if self._ac_mode else self._attr_min_temp
)
if self._window_call_cancel is not None: if self._window_call_cancel is not None:
self._window_call_cancel() self._window_call_cancel()
@@ -438,10 +446,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR) self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
self._power_temp = entry_infos.get(CONF_PRESET_POWER) self._power_temp = entry_infos.get(CONF_PRESET_POWER)
self._presence_on = ( self._presence_on = self._presence_sensor_entity_id is not None
entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
and 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 # Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
@@ -457,10 +462,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._support_flags = SUPPORT_FLAGS self._support_flags = SUPPORT_FLAGS
# Preset will be initialized from Number entities self._presets = presets
self._presets: dict[str, Any] = {} # presets self._presets_away = presets_away
self._presets_away: dict[str, Any] = {} # presets_away
_LOGGER.debug(
"%s - presets are set to: %s, away: %s",
self,
self._presets,
self._presets_away,
)
# Will be restored if possible # Will be restored if possible
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
self._saved_preset_mode = PRESET_NONE self._saved_preset_mode = PRESET_NONE
@@ -524,6 +534,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._overpowering_state = None self._overpowering_state = None
self._presence_state = None self._presence_state = None
# Calculate all possible presets
self._attr_preset_modes = [PRESET_NONE]
if len(presets):
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, _ in CONF_PRESETS.items():
if self.find_preset_temp(key) > 0:
self._attr_preset_modes.append(key)
_LOGGER.debug(
"After adding presets, preset_modes to %s", self._attr_preset_modes
)
else:
_LOGGER.debug("No preset_modes")
if self._motion_on:
self._attr_preset_modes.append(PRESET_ACTIVITY)
self._total_energy = 0 self._total_energy = 0
# Read the parameter from configuration.yaml if it exists # Read the parameter from configuration.yaml if it exists
@@ -771,9 +799,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._target_temp, self._target_temp,
self._cur_temp, self._cur_temp,
self._cur_ext_temp, self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF, self._hvac_mode == HVACMode.COOL,
) )
self.hass.create_task(self._check_initial_state())
self.reset_last_change_time() self.reset_last_change_time()
await self.get_my_previous_state() await self.get_my_previous_state()
@@ -822,18 +852,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
old_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE) old_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
# Never restore a Power or Security preset # Never restore a Power or Security preset
if old_preset_mode is not None and old_preset_mode not in HIDDEN_PRESETS: if (
# old_preset_mode in self._attr_preset_modes old_preset_mode in self._attr_preset_modes
and old_preset_mode not in HIDDEN_PRESETS
):
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE) self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
self.save_preset_mode() self.save_preset_mode()
else: else:
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
if not self._hvac_mode and old_state.state in [ if not self._hvac_mode and old_state.state:
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
]:
self._hvac_mode = old_state.state self._hvac_mode = old_state.state
else: else:
self._hvac_mode = HVACMode.OFF self._hvac_mode = HVACMode.OFF
@@ -1158,11 +1186,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
Is None if the VTherm is not controlled by central_mode""" Is None if the VTherm is not controlled by central_mode"""
return self._last_central_mode return self._last_central_mode
@property
def use_central_config_temperature(self):
"""True if this VTHerm uses the central configuration temperature"""
return self._use_central_config_temperature
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:
@@ -1181,22 +1204,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
raise NotImplementedError() raise NotImplementedError()
@overrides
async def async_turn_aux_heat_on(self) -> None: async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
raise NotImplementedError() raise NotImplementedError()
@overrides
def turn_aux_heat_off(self) -> None: def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
raise NotImplementedError() raise NotImplementedError()
@overrides
async def async_turn_aux_heat_off(self) -> None: async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
raise NotImplementedError() raise NotImplementedError()
@overrides
async def async_set_hvac_mode(self, hvac_mode: HVACMode, need_control_heating=True): async def async_set_hvac_mode(self, hvac_mode: HVACMode, need_control_heating=True):
"""Set new target hvac mode.""" """Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode) _LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
@@ -1255,8 +1274,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long
) )
old_preset_mode = self._attr_preset_mode if preset_mode == self._attr_preset_mode and not force:
if preset_mode == old_preset_mode and not force:
# 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
@@ -1289,11 +1307,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if overwrite_saved_preset: if overwrite_saved_preset:
self.save_preset_mode() self.save_preset_mode()
self.recalculate() self.recalculate()
# Notify only if there was a real change self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
if self._attr_preset_mode != old_preset_mode:
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
def reset_last_change_time( def reset_last_change_time(
self, old_preset_mode: str | None = None self, old_preset_mode: str | None = None
@@ -1308,9 +1323,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._attr_preset_mode not in HIDDEN_PRESETS self._attr_preset_mode not in HIDDEN_PRESETS
and old_preset_mode not in HIDDEN_PRESETS and old_preset_mode not in HIDDEN_PRESETS
): ):
self._last_temperature_measure = self._last_ext_temperature_measure = ( self._last_temperature_measure = (
datetime.now(tz=self._current_tz) self._last_ext_temperature_measure
) ) = datetime.now(tz=self._current_tz)
def find_preset_temp(self, preset_mode: str): def find_preset_temp(self, preset_mode: str):
"""Find the right temperature of a preset considering the presence if configured""" """Find the right temperature of a preset considering the presence if configured"""
@@ -1328,15 +1343,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if preset_mode == PRESET_POWER: if preset_mode == PRESET_POWER:
return self._power_temp return self._power_temp
if preset_mode == PRESET_ACTIVITY: if preset_mode == PRESET_ACTIVITY:
motion_preset = ( return self._presets[
self._motion_preset self._motion_preset
if self._motion_state == STATE_ON if self._motion_state == STATE_ON
else self._no_motion_preset else self._no_motion_preset
) ]
if motion_preset in self._presets:
return self._presets[motion_preset]
else:
return None
else: else:
# Select _ac presets if in COOL Mode (or over_switch with _ac_mode) # Select _ac presets if in COOL Mode (or over_switch with _ac_mode)
if self._ac_mode and self._hvac_mode == HVACMode.COOL: if self._ac_mode and self._hvac_mode == HVACMode.COOL:
@@ -1344,22 +1355,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.info("%s - find preset temp: %s", self, preset_mode) _LOGGER.info("%s - find preset temp: %s", self, preset_mode)
temp_val = self._presets.get(preset_mode, 0)
if not self._presence_on or self._presence_state in [ if not self._presence_on or self._presence_state in [
None,
STATE_ON, STATE_ON,
STATE_HOME, STATE_HOME,
]: ]:
return temp_val return self._presets[preset_mode]
else: else:
# We should return the preset_away temp val but if return self._presets_away[self.get_preset_away_name(preset_mode)]
# preset temp is 0, that means the user don't want to use
# the preset so we return 0, even if there is a value is preset_away
return (
self._presets_away.get(self.get_preset_away_name(preset_mode), 0)
if temp_val > 0
else temp_val
)
def get_preset_away_name(self, preset_mode: str) -> str: def get_preset_away_name(self, preset_mode: str) -> str:
"""Get the preset name in away mode (when presence is off)""" """Get the preset name in away mode (when presence is off)"""
@@ -1396,8 +1398,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Set the target temperature and the target temperature of underlying climate if any """Set the target temperature and the target temperature of underlying climate if any
For testing purpose you can pass an event_timestamp. For testing purpose you can pass an event_timestamp.
""" """
if temperature: self._target_temp = temperature
self._target_temp = temperature
return return
def get_state_date_or_now(self, state: State) -> datetime: def get_state_date_or_now(self, state: State) -> datetime:
@@ -1810,14 +1811,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return return
await self._async_internal_set_temperature( await self._async_internal_set_temperature(
self._presets.get( self._presets[
( self._motion_preset
self._motion_preset if self._motion_state == STATE_ON
if self._motion_state == STATE_ON else self._no_motion_preset
else self._no_motion_preset ]
),
None,
)
) )
_LOGGER.debug( _LOGGER.debug(
"%s - regarding motion, target_temp have been set to %.2f", "%s - regarding motion, target_temp have been set to %.2f",
@@ -2433,21 +2431,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"type": self._thermostat_type, "type": self._thermostat_type,
"is_controlled_by_central_mode": self.is_controlled_by_central_mode, "is_controlled_by_central_mode": self.is_controlled_by_central_mode,
"last_central_mode": self.last_central_mode, "last_central_mode": self.last_central_mode,
"frost_temp": self._presets.get(PRESET_FROST_PROTECTION, 0), "frost_temp": self._presets[PRESET_FROST_PROTECTION],
"eco_temp": self._presets.get(PRESET_ECO, 0), "eco_temp": self._presets[PRESET_ECO],
"boost_temp": self._presets.get(PRESET_BOOST, 0), "boost_temp": self._presets[PRESET_BOOST],
"comfort_temp": self._presets.get(PRESET_COMFORT, 0), "comfort_temp": self._presets[PRESET_COMFORT],
"frost_away_temp": self._presets_away.get( "frost_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_FROST_PROTECTION), 0 self.get_preset_away_name(PRESET_FROST_PROTECTION)
), ),
"eco_away_temp": self._presets_away.get( "eco_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_ECO), 0 self.get_preset_away_name(PRESET_ECO)
), ),
"boost_away_temp": self._presets_away.get( "boost_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_BOOST), 0 self.get_preset_away_name(PRESET_BOOST)
), ),
"comfort_away_temp": self._presets_away.get( "comfort_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_COMFORT), 0 self.get_preset_away_name(PRESET_COMFORT)
), ),
"power_temp": self._power_temp, "power_temp": self._power_temp,
"target_temperature_step": self.target_temperature_step, "target_temperature_step": self.target_temperature_step,
@@ -2628,87 +2626,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
def send_event(self, event_type: EventType, data: dict): def send_event(self, event_type: EventType, data: dict):
"""Send an event""" """Send an event"""
send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data) send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data)
# _LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)
async def init_presets(self, central_config): # data["entity_id"] = self.entity_id
"""Init all presets of the VTherm""" # data["name"] = self.name
# If preset central config is used and central config is set , take the presets from central config # data["state_attributes"] = self.state_attributes
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api() # self._hass.bus.fire(event_type.value, data)
presets: dict[str, Any] = {}
presets_away: dict[str, Any] = {}
def calculate_presets(items, use_central_conf_key):
presets: dict[str, Any] = {}
config_id = self._unique_id
if (
central_config
and self._entry_infos.get(use_central_conf_key, False) is True
):
config_id = central_config.entry_id
for key, preset_name in items:
_LOGGER.debug("looking for key=%s, preset_name=%s", key, preset_name)
value = vtherm_api.get_temperature_number_value(
config_id=config_id, preset_name=preset_name
)
if value is not None:
presets[key] = value
else:
_LOGGER.debug("preset_name %s not found in VTherm API", preset_name)
presets[key] = (
self._attr_max_temp if self._ac_mode else self._attr_min_temp
)
return presets
# Calculate all presets
presets = calculate_presets(
CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items(),
CONF_USE_PRESETS_CENTRAL_CONFIG,
)
if self._entry_infos.get(CONF_USE_PRESENCE_FEATURE) is True:
presets_away = calculate_presets(
(
CONF_PRESETS_AWAY_WITH_AC.items()
if self._ac_mode
else CONF_PRESETS_AWAY.items()
),
CONF_USE_PRESENCE_CENTRAL_CONFIG,
)
# aggregate all available presets now
self._presets: dict[str, Any] = presets
self._presets_away: dict[str, Any] = presets_away
# Calculate all possible presets
self._attr_preset_modes = [PRESET_NONE]
if len(self._presets):
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, _ in CONF_PRESETS.items():
if self.find_preset_temp(key) > 0:
self._attr_preset_modes.append(key)
_LOGGER.debug(
"After adding presets, preset_modes to %s", self._attr_preset_modes
)
else:
_LOGGER.debug("No preset_modes")
if self._motion_on:
self._attr_preset_modes.append(PRESET_ACTIVITY)
# Re-applicate the last preset if any to take change into account
if self._attr_preset_mode:
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
self.hass.create_task(self._check_initial_state())
async def async_turn_off(self) -> None:
await self.async_set_hvac_mode(HVACMode.OFF)
async def async_turn_on(self) -> None:
if self._ac_mode:
await self.async_set_hvac_mode(HVACMode.COOL)
else:
await self.async_set_hvac_mode(HVACMode.HEATING)

View File

@@ -7,11 +7,11 @@ from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
Event, Event,
# CoreState, CoreState,
HomeAssistantError, HomeAssistantError,
) )
from homeassistant.const import STATE_ON, STATE_OFF # , EVENT_HOMEASSISTANT_START from homeassistant.const import STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import async_track_state_change_event
@@ -386,18 +386,17 @@ class CentralBoilerBinarySensor(BinarySensorEntity):
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
api.register_central_boiler(self) api.register_central_boiler(self)
# Should be not more needed and replaced by vtherm_api.init_vtherm_links @callback
# @callback async def _async_startup_internal(*_):
# async def _async_startup_internal(*_): _LOGGER.debug("%s - Calling async_startup_internal", self)
# _LOGGER.debug("%s - Calling async_startup_internal", self) await self.listen_nb_active_vtherm_entity()
# await self.listen_nb_active_vtherm_entity()
# if self.hass.state == CoreState.running:
# if self.hass.state == CoreState.running: await _async_startup_internal()
# await _async_startup_internal() else:
# else: self.hass.bus.async_listen_once(
# self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, _async_startup_internal
# EVENT_HOMEASSISTANT_START, _async_startup_internal )
# )
async def listen_nb_active_vtherm_entity(self): async def listen_nb_active_vtherm_entity(self):
"""Initialize the listening of state change of VTherms""" """Initialize the listening of state change of VTherms"""

View File

@@ -1,5 +1,4 @@
""" Implements the VersatileThermostat climate component """ """ Implements the VersatileThermostat climate component """
import logging import logging
@@ -45,6 +44,9 @@ from .thermostat_valve import ThermostatOverValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# _LOGGER.setLevel(logging.DEBUG)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,

View File

@@ -1,5 +1,4 @@
""" Some usefull commons class """ """ Some usefull commons class """
# pylint: disable=line-too-long # pylint: disable=line-too-long
import logging import logging
@@ -183,9 +182,6 @@ class VersatileThermostatBaseEntity(Entity):
"""Returns my climate if found""" """Returns my climate if found"""
if not self._my_climate: if not self._my_climate:
self._my_climate = self.find_my_versatile_thermostat() self._my_climate = self.find_my_versatile_thermostat()
if self._my_climate:
# Only the first time
self.my_climate_is_initialized()
return self._my_climate return self._my_climate
@property @property
@@ -235,18 +231,13 @@ class VersatileThermostatBaseEntity(Entity):
) )
) )
else: else:
_LOGGER.debug("%s - no entity to listen. Try later", self) _LOGGER.warning("%s - no entity to listen. Try later", self)
self._cancel_call = async_call_later( self._cancel_call = async_call_later(
self.hass, timedelta(seconds=1), try_find_climate self.hass, timedelta(seconds=1), try_find_climate
) )
await try_find_climate(None) await try_find_climate(None)
@callback
def my_climate_is_initialized(self):
"""Called when the associated climate is initialized"""
return
@callback @callback
async def async_my_climate_changed( async def async_my_climate_changed(
self, event: Event self, event: Event

View File

@@ -74,9 +74,7 @@ def add_suggested_values_to_schema(
class VersatileThermostatBaseConfigFlow(FlowHandler): class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons.""" """The base Config flow class. Used to put some code in commons."""
VERSION = CONFIG_VERSION VERSION = 1
MINOR_VERSION = CONFIG_MINOR_VERSION
_infos: dict _infos: dict
_placeholders = { _placeholders = {
CONF_NAME: "", CONF_NAME: "",
@@ -97,22 +95,16 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._init_feature_flags(infos) self._init_feature_flags(infos)
self._init_central_config_flags(infos) self._init_central_config_flags(infos)
def _init_feature_flags(self, _): def _init_feature_flags(self, infos):
"""Fix features selection depending to infos""" """Fix features selection depending to infos"""
is_empty: bool = False # TODO remove this not bool(infos) is_empty: bool = not bool(infos)
is_central_config = (
self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
)
self._infos[CONF_USE_WINDOW_FEATURE] = ( self._infos[CONF_USE_WINDOW_FEATURE] = (
is_empty is_empty
or self._infos.get(CONF_WINDOW_SENSOR) is not None or self._infos.get(CONF_WINDOW_SENSOR) is not None
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
) )
self._infos[CONF_USE_MOTION_FEATURE] = ( self._infos[CONF_USE_MOTION_FEATURE] = (
is_empty is_empty or self._infos.get(CONF_MOTION_SENSOR) is not None
or self._infos.get(CONF_MOTION_SENSOR) is not None
or is_central_config
) )
self._infos[CONF_USE_POWER_FEATURE] = is_empty or ( self._infos[CONF_USE_POWER_FEATURE] = is_empty or (
self._infos.get(CONF_POWER_SENSOR) is not None self._infos.get(CONF_POWER_SENSOR) is not None
@@ -122,11 +114,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
) )
self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = is_empty or (
self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV) is not None
and self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV) is not None
)
def _init_central_config_flags(self, infos): def _init_central_config_flags(self, infos):
"""Initialisation of central configuration flags""" """Initialisation of central configuration flags"""
is_empty: bool = not bool(infos) is_empty: bool = not bool(infos)
@@ -141,10 +128,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
): ):
if not is_empty: if not is_empty:
current_config = self._infos.get(config, None) self._infos[config] = self._infos.get(config) is True
self._infos[config] = current_config is True or (
current_config is None and self._central_config is not None
)
else: else:
self._infos[config] = self._central_config is not None self._infos[config] = self._central_config is not None
@@ -209,7 +193,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
raise NoCentralConfig(conf) raise NoCentralConfig(conf)
# Check the service for central boiler format # Check the service for central boiler format
if self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE): if self._infos.get(CONF_ADD_CENTRAL_BOILER_CONTROL):
for conf in [ for conf in [
CONF_CENTRAL_BOILER_ACTIVATION_SRV, CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV, CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
@@ -219,92 +203,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
except ServiceConfigurationError as err: except ServiceConfigurationError as err:
raise ServiceConfigurationError(conf) from err raise ServiceConfigurationError(conf) from err
def check_config_complete(self, infos) -> bool:
"""True if the config is now complete (ie all mandatory attributes are set)"""
is_central_config = (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
)
if is_central_config:
if (
infos.get(CONF_NAME) is None
or infos.get(CONF_EXTERNAL_TEMP_SENSOR) is None
):
return False
if infos.get(CONF_USE_POWER_FEATURE, False) is True and (
infos.get(CONF_POWER_SENSOR, None) is None
or infos.get(CONF_MAX_POWER_SENSOR, None) is None
):
return False
if (
infos.get(CONF_USE_PRESENCE_FEATURE, False) is True
and infos.get(CONF_PRESENCE_SENSOR, None) is None
):
return False
else:
if (
infos.get(CONF_NAME) is None
or infos.get(CONF_TEMP_SENSOR) is None
or infos.get(CONF_CYCLE_MIN) is None
):
return False
if (
infos.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False
and infos.get(CONF_EXTERNAL_TEMP_SENSOR) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH
and infos.get(CONF_HEATER, None) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
and infos.get(CONF_CLIMATE, None) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
and infos.get(CONF_VALVE, None) is None
):
return False
if (
infos.get(CONF_USE_MOTION_FEATURE, False) is True
and infos.get(CONF_MOTION_SENSOR, None) is None
):
return False
if (
infos.get(CONF_USE_POWER_FEATURE, False) is True
and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False
and (
infos.get(CONF_POWER_SENSOR, None) is None
or infos.get(CONF_MAX_POWER_SENSOR, None) is None
)
):
return False
if (
infos.get(CONF_USE_PRESENCE_FEATURE, False) is True
and infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False
and infos.get(CONF_PRESENCE_SENSOR, None) is None
):
return False
if (
infos.get(CONF_USE_ADVANCED_CENTRAL_CONFIG, False) is False
and infos.get(CONF_MINIMAL_ACTIVATION_DELAY, -1) == -1
):
return False
return True
def merge_user_input(self, data_schema: vol.Schema, user_input: dict): def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
"""For each schema entry not in user_input, set or remove values in infos""" """For each schema entry not in user_input, set or remove values in infos"""
self._infos.update(user_input) self._infos.update(user_input)
@@ -341,15 +239,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
errors[str(err)] = "no_central_config" errors[str(err)] = "no_central_config"
except ServiceConfigurationError as err: except ServiceConfigurationError as err:
errors[str(err)] = "service_configuration_format" errors[str(err)] = "service_configuration_format"
except ConfigurationNotCompleteError as err:
errors["base"] = "configuration_not_complete"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
self.merge_user_input(data_schema, user_input) self.merge_user_input(data_schema, user_input)
# Add default values for central config flags
self._init_central_config_flags(self._infos)
_LOGGER.debug("_info is now: %s", self._infos) _LOGGER.debug("_info is now: %s", self._infos)
return await next_step_function() return await next_step_function()
@@ -370,92 +264,30 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
_LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input)
return await self.generic_step( return await self.generic_step(
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_menu "user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_main
)
async def async_step_configuration_not_complete(
self, user_input: dict | None = None
) -> FlowResult:
"""A fake step to handle the incomplete configuration flow"""
return await self.async_step_menu(user_input)
async def async_step_menu(self, user_input: dict | None = None) -> FlowResult:
"""Handle the flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_menu user_input=%s", user_input)
is_central_config = (
self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
)
menu_options = ["main", "features"]
if not is_central_config:
menu_options.append("type")
if (
self._infos.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI
or is_central_config
):
menu_options.append("tpi")
if self._infos[CONF_THERMOSTAT_TYPE] in [
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CLIMATE,
]:
menu_options.append("presets")
if (
is_central_config
and self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
):
menu_options.append("central_boiler")
if self._infos[CONF_USE_WINDOW_FEATURE] is True:
menu_options.append("window")
if self._infos[CONF_USE_MOTION_FEATURE] is True:
menu_options.append("motion")
if self._infos[CONF_USE_POWER_FEATURE] is True:
menu_options.append("power")
if self._infos[CONF_USE_PRESENCE_FEATURE] is True:
menu_options.append("presence")
menu_options.append("advanced")
if self.check_config_complete(self._infos):
menu_options.append("finalize")
else:
_LOGGER.info("The configuration is not terminated")
menu_options.append("configuration_not_complete")
return self.async_show_menu(
step_id="menu",
menu_options=menu_options,
description_placeholders=self._placeholders,
) )
async def async_step_main(self, user_input: dict | None = None) -> FlowResult: async def async_step_main(self, user_input: dict | None = None) -> FlowResult:
"""Handle the flow steps""" """Handle the flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_main user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_main user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_MAIN_DATA_SCHEMA
next_step = self.async_step_type
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
else: if user_input and user_input.get(CONF_ADD_CENTRAL_BOILER_CONTROL) is True:
next_step = self.async_step_central_boiler
else:
next_step = self.async_step_tpi
elif user_input and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_main
schema = STEP_MAIN_DATA_SCHEMA schema = STEP_MAIN_DATA_SCHEMA
# If we come from async_step_spec_main
if ( elif self._infos.get(COMES_FROM) == "async_step_spec_main":
user_input next_step = self.async_step_type
and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
):
if user_input and self._infos.get(COMES_FROM) == "async_step_spec_main":
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_main
return await self.generic_step("main", schema, user_input, next_step) return await self.generic_step("main", schema, user_input, next_step)
@@ -467,7 +299,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
else: else:
schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA
next_step = self.async_step_menu next_step = self.async_step_type
self._infos[COMES_FROM] = "async_step_spec_main" self._infos[COMES_FROM] = "async_step_spec_main"
@@ -483,7 +315,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
) )
schema = STEP_CENTRAL_BOILER_SCHEMA schema = STEP_CENTRAL_BOILER_SCHEMA
next_step = self.async_step_menu next_step = self.async_step_tpi
return await self.generic_step("central_boiler", schema, user_input, next_step) return await self.generic_step("central_boiler", schema, user_input, next_step)
@@ -493,54 +325,36 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
return await self.generic_step( return await self.generic_step(
"type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_menu "type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
) )
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE: elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
return await self.generic_step( return await self.generic_step(
"type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_menu "type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
) )
else: else:
return await self.generic_step( return await self.generic_step(
"type", "type",
STEP_THERMOSTAT_CLIMATE, STEP_THERMOSTAT_CLIMATE,
user_input, user_input,
self.async_step_menu, self.async_step_presets,
) )
async def async_step_features(self, user_input: dict | None = None) -> FlowResult:
"""Handle the Type flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_features user_input=%s", user_input)
return await self.generic_step(
"features",
(
STEP_CENTRAL_FEATURES_DATA_SCHEMA
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
else STEP_FEATURES_DATA_SCHEMA
),
user_input,
self.async_step_menu,
)
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult: async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
"""Handle the TPI flow steps""" """Handle the TPI flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_TPI_DATA_SCHEMA
next_step = (
self.async_step_spec_tpi
if user_input and user_input.get(CONF_USE_TPI_CENTRAL_CONFIG) is False
else self.async_step_presets
)
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_TPI_DATA_SCHEMA schema = STEP_CENTRAL_TPI_DATA_SCHEMA
else: next_step = self.async_step_presets
schema = STEP_TPI_DATA_SCHEMA elif self._infos.get(COMES_FROM) == "async_step_spec_tpi":
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
if (
user_input
and user_input.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
):
if user_input and self._infos.get(COMES_FROM) == "async_step_spec_tpi":
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_tpi
return await self.generic_step("tpi", schema, user_input, next_step) return await self.generic_step("tpi", schema, user_input, next_step)
@@ -550,7 +364,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
schema = STEP_CENTRAL_TPI_DATA_SCHEMA schema = STEP_CENTRAL_TPI_DATA_SCHEMA
self._infos[COMES_FROM] = "async_step_spec_tpi" self._infos[COMES_FROM] = "async_step_spec_tpi"
next_step = self.async_step_menu next_step = self.async_step_presets
return await self.generic_step("tpi", schema, user_input, next_step) return await self.generic_step("tpi", schema, user_input, next_step)
@@ -558,41 +372,82 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presets flow steps""" """Handle the presets flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input)
next_step = self.async_step_menu # advanced if self._infos.get(CONF_AC_MODE) is True:
schema_ac_or_not = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema_ac_or_not = STEP_CENTRAL_PRESETS_DATA_SCHEMA
next_step = self.async_step_advanced
schema = STEP_PRESETS_DATA_SCHEMA schema = STEP_PRESETS_DATA_SCHEMA
if self._infos[CONF_USE_WINDOW_FEATURE]:
next_step = self.async_step_window
elif self._infos[CONF_USE_MOTION_FEATURE]:
next_step = self.async_step_motion
elif self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the next step immedialty # In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
# Call directly the next step, we have nothing to display here schema = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA
return await self.async_step_window() # = self.async_step_window next_step = self.async_step_window
# If comes from async_step_spec_presets
elif self._infos.get(COMES_FROM) == "async_step_spec_presets":
schema = schema_ac_or_not
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_presets
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)
async def async_step_spec_presets(
self, user_input: dict | None = None
) -> FlowResult:
"""Handle the specific presets flow steps"""
_LOGGER.debug(
"Into ConfigFlow.async_step_spec_presets user_input=%s", user_input
)
if self._infos.get(CONF_AC_MODE) is True:
schema = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = STEP_CENTRAL_PRESETS_DATA_SCHEMA
self._infos[COMES_FROM] = "async_step_spec_presets"
next_step = self.async_step_window
# This will return to async_step_main (to keep the "main" step)
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult: async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps""" """Handle the window sensor flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_WINDOW_DATA_SCHEMA
next_step = self.async_step_advanced
if self._infos[CONF_USE_MOTION_FEATURE]:
next_step = self.async_step_motion
elif self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
else: next_step = self.async_step_motion
schema = STEP_WINDOW_DATA_SCHEMA # If comes from async_step_spec_window
elif self._infos.get(COMES_FROM) == "async_step_spec_window":
if ( # If we have a window sensor don't display the auto window parameters
user_input if self._infos.get(CONF_WINDOW_SENSOR) is not None:
and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG, False) is False schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
): else:
if ( schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
user_input elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False:
and self._infos.get(COMES_FROM) == "async_step_spec_window" next_step = self.async_step_spec_window
):
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
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_window
return await self.generic_step("window", schema, user_input, next_step) return await self.generic_step("window", schema, user_input, next_step)
@@ -619,24 +474,23 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the window and motion sensor flow steps""" """Handle the window and motion sensor flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_MOTION_DATA_SCHEMA
next_step = self.async_step_advanced
if self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
else: next_step = self.async_step_power
schema = STEP_MOTION_DATA_SCHEMA # If comes from async_step_spec_motion
elif self._infos.get(COMES_FROM) == "async_step_spec_motion":
if ( schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
user_input elif user_input and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG) is False:
and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG, False) is False next_step = self.async_step_spec_motion
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_motion"
):
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_motion
return await self.generic_step("motion", schema, user_input, next_step) return await self.generic_step("motion", schema, user_input, next_step)
@@ -652,7 +506,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._infos[COMES_FROM] = "async_step_spec_motion" self._infos[COMES_FROM] = "async_step_spec_motion"
next_step = self.async_step_menu next_step = self.async_step_power
# This will return to async_step_main (to keep the "main" step) # This will return to async_step_main (to keep the "main" step)
return await self.generic_step("motion", schema, user_input, next_step) return await self.generic_step("motion", schema, user_input, next_step)
@@ -661,24 +515,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the power management flow steps""" """Handle the power management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_POWER_DATA_SCHEMA
next_step = self.async_step_advanced
if self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_POWER_DATA_SCHEMA schema = STEP_CENTRAL_POWER_DATA_SCHEMA
else: next_step = self.async_step_presence
schema = STEP_POWER_DATA_SCHEMA # If comes from async_step_spec_motion
elif self._infos.get(COMES_FROM) == "async_step_spec_power":
if ( schema = STEP_CENTRAL_POWER_DATA_SCHEMA
user_input elif user_input and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG) is False:
and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False next_step = self.async_step_spec_power
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_power"
):
schema = STEP_CENTRAL_POWER_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_power
return await self.generic_step("power", schema, user_input, next_step) return await self.generic_step("power", schema, user_input, next_step)
@@ -690,7 +541,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._infos[COMES_FROM] = "async_step_spec_power" self._infos[COMES_FROM] = "async_step_spec_power"
next_step = self.async_step_menu next_step = self.async_step_presence
# This will return to async_step_power (to keep the "power" step) # This will return to async_step_power (to keep the "power" step)
return await self.generic_step("power", schema, user_input, next_step) return await self.generic_step("power", schema, user_input, next_step)
@@ -699,31 +550,25 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps""" """Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_PRESENCE_DATA_SCHEMA
next_step = self.async_step_advanced
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
else: next_step = self.async_step_advanced
schema = STEP_PRESENCE_DATA_SCHEMA # If comes from async_step_spec_presence
elif self._infos.get(COMES_FROM) == "async_step_spec_presence":
if ( schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
user_input elif user_input and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG) is False:
and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False next_step = self.async_step_spec_presence
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_presence"
):
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_presence
return await self.generic_step("presence", schema, user_input, next_step) return await self.generic_step("presence", schema, user_input, next_step)
async def async_step_spec_presence( async def async_step_spec_presence(
self, user_input: dict | None = None self, user_input: dict | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle the specific power flow steps""" """Handle the specific preseence flow steps"""
_LOGGER.debug( _LOGGER.debug(
"Into ConfigFlow.async_step_spec_presence user_input=%s", user_input "Into ConfigFlow.async_step_spec_presence user_input=%s", user_input
) )
@@ -732,33 +577,26 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._infos[COMES_FROM] = "async_step_spec_presence" self._infos[COMES_FROM] = "async_step_spec_presence"
next_step = self.async_step_menu next_step = self.async_step_advanced
# This will return to async_step_power (to keep the "power" step) # This will return to async_step_presence (to keep the "presence" step)
return await self.generic_step("presence", schema, user_input, next_step) return await self.generic_step("presence", schema, user_input, next_step)
async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult: async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
"""Handle the advanced parameter flow steps""" """Handle the advanced parameter flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input)
next_step = self.async_step_menu schema = STEP_ADVANCED_DATA_SCHEMA
next_step = self.async_finalize
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
else: # If comes from async_step_spec_presence
schema = STEP_ADVANCED_DATA_SCHEMA elif self._infos.get(COMES_FROM) == "async_step_spec_advanced":
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
if ( elif user_input and user_input.get(CONF_USE_ADVANCED_CENTRAL_CONFIG) is False:
user_input next_step = self.async_step_spec_advanced
and user_input.get(CONF_USE_ADVANCED_CENTRAL_CONFIG, False) is False
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_advanced"
):
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_advanced
return await self.generic_step("advanced", schema, user_input, next_step) return await self.generic_step("advanced", schema, user_input, next_step)
@@ -779,12 +617,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
# This will return to async_step_presence (to keep the "presence" step) # This will return to async_step_presence (to keep the "presence" step)
return await self.generic_step("advanced", schema, user_input, next_step) return await self.generic_step("advanced", schema, user_input, next_step)
async def async_step_finalize(self, _): async def async_finalize(self):
"""Should be implemented by Leaf classes""" """Should be implemented by Leaf classes"""
raise HomeAssistantError( raise HomeAssistantError(
"async_finalize not implemented on VersatileThermostat sub-class" "async_finalize not implemented on VersatileThermostat sub-class"
) )
# Not used but can be useful in the future
# def find_all_climates(self) -> list(str):
# """Find all climate known by HA"""
# component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
# ret: list(str) = list()
# for entity in component.entities:
# ret.append(entity.entity_id)
# _LOGGER.debug("Found all climate entities: %s", ret)
# return ret
class VersatileThermostatConfigFlow( class VersatileThermostatConfigFlow(
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
@@ -802,7 +650,7 @@ class VersatileThermostatConfigFlow(
"""Get options flow for this handler""" """Get options flow for this handler"""
return VersatileThermostatOptionsFlowHandler(config_entry) return VersatileThermostatOptionsFlowHandler(config_entry)
async def async_step_finalize(self, _): async def async_finalize(self):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
_LOGGER.debug("ConfigFlow.async_finalize") _LOGGER.debug("ConfigFlow.async_finalize")
# Removes temporary value # Removes temporary value
@@ -837,9 +685,155 @@ class VersatileThermostatOptionsFlowHandler(
CONF_NAME: self._infos[CONF_NAME], CONF_NAME: self._infos[CONF_NAME],
} }
return await self.async_step_menu(user_input) return await self.async_step_main(user_input)
async def async_step_finalize(self, _): # async def async_step_main(self, user_input: dict | None = None) -> FlowResult:
# """Handle the flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_user user_input=%s", user_input
# )
# return await self.generic_step(
# "user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_type
# )
# async def async_step_type(self, user_input: dict | None = None) -> FlowResult:
# """Handle the flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_user user_input=%s", user_input
# )
# if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
# return await self.generic_step(
# "type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
# )
# elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
# return await self.generic_step(
# "type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
# )
# else:
# return await self.generic_step(
# "type",
# STEP_THERMOSTAT_CLIMATE,
# user_input,
# self.async_step_presets,
# )
# async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
# """Handle the tpi flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_tpi user_input=%s", user_input
# )
# return await self.generic_step(
# "tpi", STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets
# )
# async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
# """Handle the presets flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_presets user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_WINDOW_FEATURE]:
# next_step = self.async_step_window
# elif self._infos[CONF_USE_MOTION_FEATURE]:
# next_step = self.async_step_motion
# elif self._infos[CONF_USE_POWER_FEATURE]:
# next_step = self.async_step_power
# elif self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# if self._infos.get(CONF_AC_MODE) is True:
# schema = STEP_PRESETS_WITH_AC_DATA_SCHEMA
# else:
# schema = STEP_PRESETS_DATA_SCHEMA
# return await self.generic_step("presets", schema, user_input, next_step)
# async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
# """Handle the window sensor flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_window user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_MOTION_FEATURE]:
# next_step = self.async_step_motion
# elif self._infos[CONF_USE_POWER_FEATURE]:
# next_step = self.async_step_power
# elif self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# return await self.generic_step(
# "window", STEP_WINDOW_DATA_SCHEMA, user_input, next_step
# )
# async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
# """Handle the window and motion sensor flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_motion user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_POWER_FEATURE]:
# next_step = self.async_step_power
# elif self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# return await self.generic_step(
# "motion", STEP_MOTION_DATA_SCHEMA, user_input, next_step
# )
# async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
# """Handle the power management flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_power user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# return await self.generic_step(
# "power",
# STEP_POWER_DATA_SCHEMA,
# user_input,
# next_step,
# )
# async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
# """Handle the presence management flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
# )
# if self._infos.get(CONF_AC_MODE) is True:
# schema = STEP_PRESENCE_WITH_AC_DATA_SCHEMA
# else:
# schema = STEP_PRESENCE_DATA_SCHEMA
# return await self.generic_step(
# "presence",
# schema,
# user_input,
# self.async_step_advanced,
# )
# async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
# """Handle the advanced flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_advanced user_input=%s", user_input
# )
# return await self.generic_step(
# "advanced",
# STEP_ADVANCED_DATA_SCHEMA,
# user_input,
# self.async_end,
# )
async def async_finalize(self):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
if not self._infos[CONF_USE_WINDOW_FEATURE]: if not self._infos[CONF_USE_WINDOW_FEATURE]:
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False

View File

@@ -44,31 +44,16 @@ 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.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean,
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean, vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): 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_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean, vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
} }
) )
STEP_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
}
)
STEP_CENTRAL_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_CENTRAL_BOILER_FEATURE, default=False): cv.boolean,
}
)
STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector( vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
@@ -77,6 +62,7 @@ STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float), vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float), vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float), vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
vol.Required(CONF_ADD_CENTRAL_BOILER_CONTROL, default=False): cv.boolean,
} }
) )
@@ -112,7 +98,6 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
vol.Optional(CONF_HEATER_4): selector.EntitySelector( vol.Optional(CONF_HEATER_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]), selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
), ),
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In( vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[ [
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
@@ -206,6 +191,18 @@ STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
} }
) )
STEP_CENTRAL_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{vol.Optional(v, default=0): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
)
STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA = (
vol.Schema( # pylint: disable=invalid-name # pylint: disable=invalid-name
{
vol.Optional(v, default=0): vol.Coerce(float)
for (k, v) in CONF_PRESETS_WITH_AC.items()
}
)
)
STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
@@ -253,7 +250,7 @@ STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid
STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_MOTION_SENSOR): selector.EntitySelector( vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig( selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN] domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
), ),
@@ -285,10 +282,10 @@ STEP_CENTRAL_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_POWER_SENSOR): selector.EntitySelector( vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
), ),
vol.Required(CONF_MAX_POWER_SENSOR): selector.EntitySelector( vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
), ),
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float), vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
@@ -303,7 +300,19 @@ STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_PRESENCE_SENSOR): selector.EntitySelector( vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY.items()
}
)
STEP_CENTRAL_PRESENCE_WITH_AC_DATA_SCHEMA = { # pylint: disable=invalid-name
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY_WITH_AC.items()
}
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig( selector.EntitySelectorConfig(
domain=[ domain=[
PERSON_DOMAIN, PERSON_DOMAIN,
@@ -311,12 +320,7 @@ STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
INPUT_BOOLEAN_DOMAIN, INPUT_BOOLEAN_DOMAIN,
] ]
), ),
) ),
},
)
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean, vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean,
} }
) )

View File

@@ -22,10 +22,6 @@ from .prop_algorithm import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_VERSION = 1
CONFIG_MINOR_VERSION = 2
PRESET_TEMP_SUFFIX = "_temp"
PRESET_AC_SUFFIX = "_ac" PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
@@ -43,13 +39,11 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat" DOMAIN = "versatile_thermostat"
# The order is important.
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.NUMBER,
Platform.SELECT, Platform.SELECT,
Platform.CLIMATE, Platform.CLIMATE,
Platform.SENSOR, Platform.SENSOR,
# Number should be after CLIMATE
Platform.NUMBER,
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
] ]
@@ -57,7 +51,6 @@ CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id" CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id" CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id" CONF_HEATER_4 = "heater_entity4_id"
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id" CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id" CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id" CONF_POWER_SENSOR = "power_sensor_entity_id"
@@ -95,7 +88,6 @@ CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature" CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature" CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature" CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
CONF_AC_MODE = "ac_mode" CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold" CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold" CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
@@ -138,6 +130,7 @@ CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
CONF_USE_CENTRAL_MODE = "use_central_mode" CONF_USE_CENTRAL_MODE = "use_central_mode"
CONF_ADD_CENTRAL_BOILER_CONTROL = "add_central_boiler_control"
CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service" CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service"
CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service" CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
@@ -152,7 +145,7 @@ DEFAULT_SHORT_EMA_PARAMS = {
} }
CONF_PRESETS = { CONF_PRESETS = {
p: f"{p}{PRESET_TEMP_SUFFIX}" p: f"{p}_temp"
for p in ( for p in (
PRESET_FROST_PROTECTION, PRESET_FROST_PROTECTION,
PRESET_ECO, PRESET_ECO,
@@ -162,7 +155,7 @@ CONF_PRESETS = {
} }
CONF_PRESETS_WITH_AC = { CONF_PRESETS_WITH_AC = {
p: f"{p}{PRESET_TEMP_SUFFIX}" p: f"{p}_temp"
for p in ( for p in (
PRESET_FROST_PROTECTION, PRESET_FROST_PROTECTION,
PRESET_ECO, PRESET_ECO,
@@ -178,7 +171,7 @@ CONF_PRESETS_WITH_AC = {
PRESET_AWAY_SUFFIX = "_away" PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = { CONF_PRESETS_AWAY = {
p: f"{p}{PRESET_TEMP_SUFFIX}" p: f"{p}_temp"
for p in ( for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX, PRESET_ECO + PRESET_AWAY_SUFFIX,
@@ -188,7 +181,7 @@ CONF_PRESETS_AWAY = {
} }
CONF_PRESETS_AWAY_WITH_AC = { CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}{PRESET_TEMP_SUFFIX}" p: f"{p}_temp"
for p in ( for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX, PRESET_ECO + PRESET_AWAY_SUFFIX,
@@ -219,7 +212,6 @@ ALL_CONF = (
CONF_HEATER_2, CONF_HEATER_2,
CONF_HEATER_3, CONF_HEATER_3,
CONF_HEATER_4, CONF_HEATER_4,
CONF_HEATER_KEEP_ALIVE,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR, CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
@@ -256,7 +248,6 @@ ALL_CONF = (
CONF_USE_MOTION_FEATURE, CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE, CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE, CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_AC_MODE, CONF_AC_MODE,
CONF_VALVE, CONF_VALVE,
CONF_VALVE_2, CONF_VALVE_2,
@@ -277,6 +268,7 @@ ALL_CONF = (
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_USE_CENTRAL_MODE,
CONF_ADD_CENTRAL_BOILER_CONTROL,
CONF_USED_BY_CENTRAL_BOILER, CONF_USED_BY_CENTRAL_BOILER,
CONF_CENTRAL_BOILER_ACTIVATION_SRV, CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV, CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
@@ -329,7 +321,7 @@ CONF_WINDOW_ACTIONS = [
CONF_WINDOW_ECO_TEMP, CONF_WINDOW_ECO_TEMP,
] ]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
@@ -367,9 +359,7 @@ CENTRAL_MODES = [
class RegulationParamSlow: class RegulationParamSlow:
"""Light parameters for slow latency regulation""" """Light parameters for slow latency regulation"""
kp: float = ( kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
)
ki: float = ( ki: float = (
0.8 / 288.0 0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours ) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
@@ -377,9 +367,7 @@ class RegulationParamSlow:
1.0 / 25.0 1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor ) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: float = ( stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
)
accumulated_error_threshold: float = ( accumulated_error_threshold: float = (
2.0 * 288 2.0 * 288
) # this allows up to 2°C long term offset in both directions ) # this allows up to 2°C long term offset in both directions
@@ -471,10 +459,6 @@ class ServiceConfigurationError(HomeAssistantError):
"""Error in the service configuration to control the central boiler""" """Error in the service configuration to control the central boiler"""
class ConfigurationNotCompleteError(HomeAssistantError):
"""Error the configuration is not complete"""
class overrides: # pylint: disable=invalid-name class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides""" """An annotation to inform overrides"""

View File

@@ -1,54 +0,0 @@
"""Building blocks for the heater switch keep-alive feature.
The heater switch keep-alive feature consists of regularly refreshing the state
of directly controlled switches at a configurable interval (regularly turning the
switch 'on' or 'off' again even if it is already turned 'on' or 'off'), just like
the keep_alive setting of Home Assistant's Generic Thermostat integration:
https://www.home-assistant.io/integrations/generic_thermostat/
"""
import logging
from collections.abc import Awaitable, Callable
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
class IntervalCaller:
"""Repeatedly call a given async action function at a given regular interval.
Convenience wrapper around Home Assistant's `async_track_time_interval` function.
"""
def __init__(self, hass: HomeAssistant, interval_sec: int) -> None:
self._hass = hass
self._interval_sec = interval_sec
self._remove_handle: CALLBACK_TYPE | None = None
def cancel(self):
"""Cancel the regular calls to the action function."""
if self._remove_handle:
self._remove_handle()
self._remove_handle = None
def set_async_action(self, action: Callable[[], Awaitable[None]]):
"""Set the async action function to be called at regular intervals."""
if not self._interval_sec:
return
self.cancel()
async def callback(_time: datetime):
try:
_LOGGER.debug("Calling keep-alive action")
await action()
except Exception as e: # pylint: disable=broad-exception-caught
_LOGGER.error(e)
self.cancel()
self._remove_handle = async_track_time_interval(
self._hass, callback, timedelta(seconds=self._interval_sec)
)

View File

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

View File

@@ -6,75 +6,24 @@ import logging
# from homeassistant.const import EVENT_HOMEASSISTANT_START # from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState # , callback from homeassistant.core import HomeAssistant, CoreState # , callback
from homeassistant.components.number import ( from homeassistant.components.number import NumberEntity, NumberMode
NumberEntity,
NumberMode,
NumberDeviceClass,
DOMAIN as NUMBER_DOMAIN,
)
from homeassistant.components.climate import (
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
)
from homeassistant.components.sensor import UnitOfTemperature
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from .vtherm_api import VersatileThermostatAPI
from .commons import VersatileThermostatBaseEntity
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICE_MANUFACTURER, DEVICE_MANUFACTURER,
CONF_NAME, CONF_NAME,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_TEMP_MIN, CONF_ADD_CENTRAL_BOILER_CONTROL,
CONF_TEMP_MAX,
CONF_STEP_TEMPERATURE,
CONF_AC_MODE,
PRESET_FROST_PROTECTION,
PRESET_ECO_AC,
PRESET_COMFORT_AC,
PRESET_BOOST_AC,
PRESET_AWAY_SUFFIX,
PRESET_TEMP_SUFFIX,
CONF_PRESETS_VALUES,
CONF_PRESETS_WITH_AC_VALUES,
CONF_PRESETS_AWAY_VALUES,
CONF_PRESETS_AWAY_WITH_AC_VALUES,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESENCE_FEATURE,
overrides, overrides,
) )
PRESET_ICON_MAPPING = {
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer",
PRESET_ECO + PRESET_TEMP_SUFFIX: "mdi:leaf",
PRESET_COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa",
PRESET_BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch",
PRESET_ECO_AC + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline",
PRESET_COMFORT_AC + PRESET_TEMP_SUFFIX: "mdi:sofa-outline",
PRESET_BOOST_AC + PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline",
PRESET_FROST_PROTECTION
+ PRESET_AWAY_SUFFIX
+ PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer",
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:leaf",
PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:sofa",
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:rocket-launch",
PRESET_ECO_AC + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline",
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:sofa-outline",
PRESET_BOOST_AC
+ PRESET_AWAY_SUFFIX
+ PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline",
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -91,99 +40,19 @@ async def async_setup_entry(
unique_id = entry.entry_id unique_id = entry.entry_id
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
# is_central_boiler = entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE) is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL)
entities = [] if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG or not is_central_boiler:
return
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG: entities = [
# Creates non central temperature entities ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data),
if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False): ]
if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_WITH_AC_VALUES:
_LOGGER.warning(
"%s - configuring Number non central, AC, non AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, True, False, entry.data
)
)
else:
for preset in CONF_PRESETS_VALUES:
_LOGGER.warning(
"%s - configuring Number non central, non AC, non AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, False, False, entry.data
)
)
if entry.data.get(
CONF_USE_PRESENCE_FEATURE, False
) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False):
if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
_LOGGER.warning(
"%s - configuring Number non central, AC, AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, True, True, entry.data
)
)
else:
for preset in CONF_PRESETS_AWAY_VALUES:
_LOGGER.warning(
"%s - configuring Number non central, non AC, AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, False, True, entry.data
)
)
# For central config only
else:
entities.append(
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
)
for preset in CONF_PRESETS_WITH_AC_VALUES:
_LOGGER.warning(
"%s - configuring Number central, AC, non AWAY for preset %s",
name,
preset,
)
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, name, preset, True, False, entry.data
)
)
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
_LOGGER.warning(
"%s - configuring Number central, AC, AWAY for preset %s", name, preset
)
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, name, preset, True, True, entry.data
)
)
async_add_entities(entities, True) async_add_entities(entities, True)
class ActivateBoilerThresholdNumber( class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity):
NumberEntity, RestoreEntity
): # pylint: disable=abstract-method
"""Representation of the threshold of the number of VTherm """Representation of the threshold of the number of VTherm
which should be active to activate the boiler""" which should be active to activate the boiler"""
@@ -246,239 +115,3 @@ class ActivateBoilerThresholdNumber(
def __str__(self): def __str__(self):
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"
class CentralConfigTemperatureNumber(
NumberEntity, RestoreEntity
): # pylint: disable=abstract-method
"""Representation of one temperature number"""
_attr_has_entity_name = True
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
entry_infos,
) -> None:
"""Initialize the temperature with entry_infos if available. Else
the restoration will do the trick."""
self._config_id = unique_id
self._device_name = name
# self._attr_name = name
self._attr_translation_key = preset_name
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
self._attr_unique_id = f"central_configuration_preset_{preset_name}"
self._attr_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX)
# Initialize the values if included into the entry_infos. This will do
# the temperature migration. Else the temperature will be restored from
# previous value
# TODO remove this after the next major release and just keep the init min/max
temp = None
if (temp := entry_infos.get(preset_name, None)) is not None:
self._attr_value = self._attr_native_value = temp
else:
if entry_infos.get(CONF_AC_MODE) is True:
self._attr_native_value = self._attr_native_max_value
else:
self._attr_native_value = self._attr_native_min_value
self._attr_mode = NumberMode.BOX
self._preset_name = preset_name
self._is_away = is_away
self._is_ac = is_ac
@property
def icon(self) -> str | None:
return PRESET_ICON_MAPPING[self._preset_name]
@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()
# register the temp entity for this device and preset
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
api.register_temperature_number(self._config_id, self._preset_name, self)
# Restore value from previous one if exists
old_state: CoreState = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
try:
if old_state is not None and ((value := float(old_state.state)) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@overrides
async def async_set_native_value(self, value: float) -> None:
"""The value have change from the Number Entity in UI"""
float_value = float(value)
old_value = float(self._attr_native_value)
if float_value == old_value:
return
self._attr_value = self._attr_native_value = float_value
# persist the value
self.async_write_ha_state()
# We have to reload all VTherm for which uses the central configuration
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
# Update the VTherms which have temperature in central config
self.hass.create_task(api.init_vtherm_links(only_use_central=True))
def __str__(self):
return f"VersatileThermostat-{self.name}"
@property
def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement"""
# TODO Kelvin ? It seems not because all internal values are stored in
# ° Celsius but only the render in front can be in °K depending on the
# user configuration.
return UnitOfTemperature.CELSIUS
class TemperatureNumber( # pylint: disable=abstract-method
VersatileThermostatBaseEntity, NumberEntity, RestoreEntity
):
"""Representation of one temperature number"""
_attr_has_entity_name = True
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
entry_infos,
) -> None:
"""Initialize the temperature with entry_infos if available. Else
the restoration will do the trick."""
super().__init__(hass, unique_id, name)
self._attr_translation_key = preset_name
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
self._attr_unique_id = f"{self._device_name}_preset_{preset_name}"
self._attr_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX)
# Initialize the values if included into the entry_infos. This will do
# the temperature migration.
temp = None
if (temp := entry_infos.get(preset_name, None)) is not None:
self._attr_value = self._attr_native_value = temp
else:
if entry_infos.get(CONF_AC_MODE) is True:
self._attr_native_value = self._attr_native_max_value
else:
self._attr_native_value = self._attr_native_min_value
self._attr_mode = NumberMode.BOX
self._preset_name = preset_name
self._canonical_preset_name = preset_name.replace(
PRESET_TEMP_SUFFIX, ""
).replace(PRESET_AWAY_SUFFIX, "")
self._is_away = is_away
self._is_ac = is_ac
@property
def icon(self) -> str | None:
return PRESET_ICON_MAPPING[self._preset_name]
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
# register the temp entity for this device and preset
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
api.register_temperature_number(self._config_id, self._preset_name, self)
old_state: CoreState = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
try:
if old_state is not None and ((value := float(old_state.state)) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@overrides
def my_climate_is_initialized(self):
"""Called when the associated climate is initialized"""
self._attr_native_step = self.my_climate.target_temperature_step
self._attr_native_min_value = self.my_climate.min_temp
self._attr_native_max_value = self.my_climate.max_temp
return
@overrides
async def async_set_native_value(self, value: float) -> None:
"""Change the value"""
if self.my_climate is None:
_LOGGER.warning(
"%s - cannot change temperature because VTherm is not initialized", self
)
return
float_value = float(value)
old_value = float(self._attr_native_value)
if float_value == old_value:
return
self._attr_value = self._attr_native_value = float_value
self.async_write_ha_state()
# Update the VTherm temp
self.hass.create_task(
self.my_climate.service_set_preset_temperature(
self._canonical_preset_name,
self._attr_native_value if not self._is_away else None,
self._attr_native_value if self._is_away else None,
)
)
def __str__(self):
return f"VersatileThermostat-{self.name}"
@property
def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement"""
if not self.my_climate:
return UnitOfTemperature.CELSIUS
return self.my_climate.temperature_unit

View File

@@ -47,10 +47,10 @@ class PITemperatureRegulator:
def set_target_temp(self, target_temp): def set_target_temp(self, target_temp):
"""Set the new target_temp""" """Set the new target_temp"""
self.target_temp = target_temp self.target_temp = target_temp
# Do not reset the accumulated error
# Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now. # Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now.
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed if self.accumulated_error < 0:
# if self.accumulated_error < 0: self.accumulated_error = 0
# self.accumulated_error = 0
def calculate_regulated_temperature( def calculate_regulated_temperature(
self, room_temp: float, external_temp: float self, room_temp: float, external_temp: float
@@ -71,11 +71,6 @@ class PITemperatureRegulator:
error = self.target_temp - room_temp error = self.target_temp - room_temp
# Calculate the sum of error (I) # Calculate the sum of error (I)
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
# If the error have change its sign, reset smoothly the accumulated error
if error * self.accumulated_error < 0:
self.accumulated_error = self.accumulated_error / 2.0
self.accumulated_error += error self.accumulated_error += error
# Capping of the error # Capping of the error

View File

@@ -1,8 +1,6 @@
""" The TPI calculation module """ """ The TPI calculation module """
import logging import logging
from homeassistant.components.climate import HVACMode
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PROPORTIONAL_FUNCTION_ATAN = "atan" PROPORTIONAL_FUNCTION_ATAN = "atan"
@@ -48,20 +46,19 @@ class PropAlgorithm:
def calculate( def calculate(
self, self,
target_temp: float | None, target_temp: float,
current_temp: float | None, current_temp: float,
ext_current_temp: float | None, ext_current_temp: float,
hvac_mode: HVACMode, cooling=False,
): ):
"""Do the calculation of the duration""" """Do the calculation of the duration"""
if target_temp is None or current_temp is None: if target_temp is None or current_temp is None:
log = _LOGGER.debug if hvac_mode == HVACMode.OFF else _LOGGER.warning _LOGGER.warning(
log(
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
) )
self._calculated_on_percent = 0 self._calculated_on_percent = 0
else: else:
if hvac_mode == HVACMode.COOL: if cooling:
delta_temp = current_temp - target_temp delta_temp = current_temp - target_temp
delta_ext_temp = ( delta_ext_temp = (
ext_current_temp ext_current_temp

View File

@@ -49,7 +49,7 @@ from .const import (
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_USE_CENTRAL_BOILER_FEATURE, CONF_ADD_CENTRAL_BOILER_CONTROL,
overrides, overrides,
) )
@@ -75,7 +75,7 @@ async def async_setup_entry(
entities = None entities = None
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE): if entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL):
entities = [ entities = [
NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data) NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data)
] ]

View File

@@ -12,32 +12,13 @@
"thermostat_type": "Only one central configuration type is possible" "thermostat_type": "Only one central configuration type is possible"
} }
}, },
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Add new Versatile Thermostat", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"thermostat_type": "Thermostat type", "thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
@@ -45,22 +26,16 @@
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features",
"description": "Thermostat features",
"data": {
"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",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -71,7 +46,6 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -94,7 +68,6 @@
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -129,9 +102,26 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", "description": "For each preset set the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
@@ -197,10 +187,25 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" "eco_away_temp": "Eco preset",
"comfort_away_temp": "Comfort preset",
"boost_away_temp": "Boost preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco preset in AC mode",
"comfort_ac_away_temp": "Comfort preset in AC mode",
"boost_ac_away_temp": "Boost pres et in AC mode",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id" "presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
@@ -244,25 +249,6 @@
"thermostat_type": "Only one central configuration type is possible" "thermostat_type": "Only one central configuration type is possible"
} }
}, },
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Main - {name}", "title": "Main - {name}",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
@@ -277,22 +263,16 @@
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features - {name}",
"description": "Thermostat features",
"data": {
"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",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -303,7 +283,6 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -326,7 +305,6 @@
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -361,9 +339,26 @@
}, },
"presets": { "presets": {
"title": "Presets - {name}", "title": "Presets - {name}",
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", "description": "For each preset set the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
@@ -429,10 +424,25 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" "eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco 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",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id" "presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
@@ -523,53 +533,6 @@
} }
} }
} }
},
"number": {
"frost_temp": {
"name": "Frost"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Comfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Frost ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Comfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Frost away"
},
"eco_away_temp": {
"name": "Eco away"
},
"comfort_away_temp": {
"name": "Comfort away"
},
"boost_away_temp": {
"name": "Boost away"
},
"eco_ac_away_temp": {
"name": "Eco ac away"
},
"comfort_ac_away_temp": {
"name": "Comfort ac away"
},
"boost_ac_away_temp": {
"name": "Boost ac away"
}
} }
} }
} }

View File

@@ -216,7 +216,7 @@ class ThermostatOverClimate(BaseThermostat):
): ):
offset_temp = device_temp - self.current_temperature offset_temp = device_temp - self.current_temperature
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, self._auto_regulation_dtemp) target_temp = self.regulated_target_temp + offset_temp
_LOGGER.debug( _LOGGER.debug(
"%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f", "%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f",
@@ -491,9 +491,9 @@ class ThermostatOverClimate(BaseThermostat):
super().update_custom_attributes() super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
self._attr_extra_state_attributes["start_hvac_action_date"] = ( self._attr_extra_state_attributes[
self._underlying_climate_start_hvac_action_date "start_hvac_action_date"
) ] = self._underlying_climate_start_hvac_action_date
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[ self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
0 0
].entity_id ].entity_id
@@ -509,32 +509,32 @@ class ThermostatOverClimate(BaseThermostat):
if self.is_regulated: if self.is_regulated:
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
self._attr_extra_state_attributes["regulated_target_temperature"] = ( self._attr_extra_state_attributes[
self._regulated_target_temp "regulated_target_temperature"
) ] = self._regulated_target_temp
self._attr_extra_state_attributes["auto_regulation_mode"] = ( self._attr_extra_state_attributes[
self.auto_regulation_mode "auto_regulation_mode"
) ] = self.auto_regulation_mode
self._attr_extra_state_attributes["regulation_accumulated_error"] = ( self._attr_extra_state_attributes[
self._regulation_algo.accumulated_error "regulation_accumulated_error"
) ] = self._regulation_algo.accumulated_error
self._attr_extra_state_attributes["auto_fan_mode"] = self.auto_fan_mode self._attr_extra_state_attributes["auto_fan_mode"] = self.auto_fan_mode
self._attr_extra_state_attributes["current_auto_fan_mode"] = ( self._attr_extra_state_attributes[
self._current_auto_fan_mode "current_auto_fan_mode"
) ] = self._current_auto_fan_mode
self._attr_extra_state_attributes["auto_activated_fan_mode"] = ( self._attr_extra_state_attributes[
self._auto_activated_fan_mode "auto_activated_fan_mode"
) ] = self._auto_activated_fan_mode
self._attr_extra_state_attributes["auto_deactivated_fan_mode"] = ( self._attr_extra_state_attributes[
self._auto_deactivated_fan_mode "auto_deactivated_fan_mode"
) ] = self._auto_deactivated_fan_mode
self._attr_extra_state_attributes["auto_regulation_use_device_temp"] = ( self._attr_extra_state_attributes[
self.auto_regulation_use_device_temp "auto_regulation_use_device_temp"
) ] = self.auto_regulation_use_device_temp
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(

View File

@@ -15,7 +15,6 @@ from .const import (
CONF_HEATER_2, CONF_HEATER_2,
CONF_HEATER_3, CONF_HEATER_3,
CONF_HEATER_4, CONF_HEATER_4,
CONF_HEATER_KEEP_ALIVE,
CONF_INVERSE_SWITCH, CONF_INVERSE_SWITCH,
overrides, overrides,
) )
@@ -106,7 +105,6 @@ class ThermostatOverSwitch(BaseThermostat):
thermostat=self, thermostat=self,
switch_entity_id=switch, switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle, initial_delay_sec=idx * delta_cycle,
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
) )
) )
@@ -127,7 +125,6 @@ class ThermostatOverSwitch(BaseThermostat):
self.hass, [switch.entity_id], self._async_switch_changed self.hass, [switch.entity_id], self._async_switch_changed
) )
) )
switch.startup()
self.hass.create_task(self.async_control_heating()) self.hass.create_task(self.async_control_heating())
@@ -183,7 +180,7 @@ class ThermostatOverSwitch(BaseThermostat):
self._target_temp, self._target_temp,
self._cur_temp, self._cur_temp,
self._cur_ext_temp, self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF, self._hvac_mode == HVACMode.COOL,
) )
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()

View File

@@ -31,7 +31,7 @@ from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method 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 = ( _entity_component_unrecorded_attributes = (
@@ -234,7 +234,7 @@ class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
self._target_temp, self._target_temp,
self._cur_temp, self._cur_temp,
self._cur_ext_temp, self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF, self._hvac_mode == HVACMode.COOL,
) )
new_valve_percent = round( new_valve_percent = round(

View File

@@ -12,32 +12,13 @@
"thermostat_type": "Only one central configuration type is possible" "thermostat_type": "Only one central configuration type is possible"
} }
}, },
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Add new Versatile Thermostat", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"thermostat_type": "Thermostat type", "thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
@@ -45,22 +26,16 @@
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features",
"description": "Thermostat features",
"data": {
"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",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -71,7 +46,6 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -94,7 +68,6 @@
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -129,9 +102,26 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", "description": "For each preset set the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
@@ -197,10 +187,25 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" "eco_away_temp": "Eco preset",
"comfort_away_temp": "Comfort preset",
"boost_away_temp": "Boost preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco preset in AC mode",
"comfort_ac_away_temp": "Comfort preset in AC mode",
"boost_ac_away_temp": "Boost pres et in AC mode",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id" "presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
@@ -244,25 +249,6 @@
"thermostat_type": "Only one central configuration type is possible" "thermostat_type": "Only one central configuration type is possible"
} }
}, },
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Main - {name}", "title": "Main - {name}",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
@@ -277,22 +263,16 @@
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features - {name}",
"description": "Thermostat features",
"data": {
"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",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -303,7 +283,6 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -326,7 +305,6 @@
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -361,9 +339,26 @@
}, },
"presets": { "presets": {
"title": "Presets - {name}", "title": "Presets - {name}",
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", "description": "For each preset set the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
@@ -429,10 +424,25 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" "eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco 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",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id" "presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
@@ -523,53 +533,6 @@
} }
} }
} }
},
"number": {
"frost_temp": {
"name": "Frost"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Comfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Frost ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Comfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Frost away"
},
"eco_away_temp": {
"name": "Eco away"
},
"comfort_away_temp": {
"name": "Comfort away"
},
"boost_away_temp": {
"name": "Boost away"
},
"eco_ac_away_temp": {
"name": "Eco ac away"
},
"comfort_ac_away_temp": {
"name": "Comfort ac away"
},
"boost_ac_away_temp": {
"name": "Boost ac away"
}
} }
} }
} }

View File

@@ -12,25 +12,6 @@
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible." "thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
} }
}, },
"menu": {
"title": "Menu",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": {
"main": "Principaux Attributs",
"central_boiler": "Chauffage central",
"type": "Sous-jacents",
"tpi": "Paramètres TPI",
"features": "Fonctions",
"presets": "Pre-réglages",
"window": "Détection d'ouverture",
"motion": "Détection de mouvement",
"power": "Gestion de la puissance",
"presence": "Détection de présence",
"advanced": "Paramètres avancés",
"finalize": "Finaliser la création",
"configuration_not_complete": "Configuration incomplète"
}
},
"main": { "main": {
"title": "Ajout d'un nouveau thermostat", "title": "Ajout d'un nouveau thermostat",
"description": "Principaux attributs obligatoires", "description": "Principaux attributs obligatoires",
@@ -45,22 +26,16 @@
"step_temperature": "Pas de température", "step_temperature": "Pas de température",
"device_power": "Puissance de l'équipement", "device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.", "use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure."
}
},
"features": {
"title": "Fonctions",
"description": "Fonctions du thermostat à utiliser",
"data": {
"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",
"use_presence_feature": "Avec détection de présence", "use_presence_feature": "Avec détection de présence",
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante." "use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
"add_central_boiler_control": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure."
} }
}, },
"type": { "type": {
@@ -71,7 +46,6 @@
"heater_entity2_id": "2ème radiateur", "heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur", "heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur", "heater_entity4_id": "4ème radiateur",
"heater_keep_alive": "keep-alive (sec)",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent", "climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent", "climate_entity2_id": "2ème thermostat sous-jacent",
@@ -94,7 +68,6 @@
"heater_entity2_id": "Optionnel entity id du 2ème radiateur", "heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur", "heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur", "heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent", "climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent", "climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
@@ -128,10 +101,27 @@
} }
}, },
"presets": { "presets": {
"title": "Pre-réglages", "title": "Presets",
"description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques", "description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
"data": { "data": {
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale" "eco_temp": "Preset Eco",
"comfort_temp": "Preset Comfort",
"boost_temp": "Preset Boost",
"frost_temp": "Preset Hors-gel",
"eco_ac_temp": "Preset Eco en mode AC",
"comfort_ac_temp": "Preset Comfort en mode AC",
"boost_ac_temp": "Preset Boost en mode AC",
"use_presets_central_config": "Utiliser la configuration des presets centrale"
},
"data_description": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost",
"frost_temp": "Température en preset Hors-gel",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC",
"use_presets_central_config": "Cochez pour utiliser la configuration des presets centrale. Décochez et saisissez les attributs pour utiliser une configuration des presets spécifique"
} }
}, },
"window": { "window": {
@@ -194,13 +184,28 @@
}, },
"presence": { "presence": {
"title": "Gestion de la présence", "title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.", "description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"data": { "data": {
"presence_sensor_entity_id": "Capteur de présence", "presence_sensor_entity_id": "Capteur de présence",
"use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées" "eco_away_temp": "preset Eco",
"comfort_away_temp": "preset Comfort",
"boost_away_temp": "preset Boost",
"frost_away_temp": "preset Hors-gel",
"eco_ac_away_temp": "preset Eco en mode AC",
"comfort_ac_away_temp": "preset Comfort en mode AC",
"boost_ac_away_temp": "preset Boost en mode AC",
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence" "presence_sensor_entity_id": "Id d'entité du capteur de présence",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence",
"frost_away_temp": "Température en preset Hors-gel en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
} }
}, },
"advanced": { "advanced": {
@@ -256,25 +261,6 @@
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible." "thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
} }
}, },
"menu": {
"title": "Menu",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": {
"main": "Principaux Attributs",
"central_boiler": "Chauffage central",
"type": "Sous-jacents",
"tpi": "Paramètres TPI",
"features": "Fonctions",
"presets": "Pre-réglages",
"window": "Détection d'ouvertures",
"motion": "Détection de mouvement",
"power": "Gestion de la puissance",
"presence": "Détection de présence",
"advanced": "Paramètres avancés",
"finalize": "Finaliser les modifications",
"configuration_not_complete": "Configuration incomplète"
}
},
"main": { "main": {
"title": "Attributs - {name}", "title": "Attributs - {name}",
"description": "Principaux attributs obligatoires", "description": "Principaux attributs obligatoires",
@@ -289,22 +275,16 @@
"step_temperature": "Pas de température", "step_temperature": "Pas de température",
"device_power": "Puissance de l'équipement", "device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.", "use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...).",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée."
}
},
"features": {
"title": "Fonctions - {name}",
"description": "Fonctions du thermostat à utiliser",
"data": {
"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",
"use_presence_feature": "Avec détection de présence", "use_presence_feature": "Avec détection de présence",
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante." "use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
"add_central_boiler_control": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée."
} }
}, },
"type": { "type": {
@@ -315,7 +295,6 @@
"heater_entity2_id": "2ème radiateur", "heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur", "heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur", "heater_entity4_id": "4ème radiateur",
"heater_keep_alive": "Keep-alive (sec)",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent", "climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent", "climate_entity2_id": "2ème thermostat sous-jacent",
@@ -338,7 +317,6 @@
"heater_entity2_id": "Optionnel entity id du 2ème radiateur", "heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur", "heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur", "heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent", "climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent", "climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
@@ -367,9 +345,26 @@
}, },
"presets": { "presets": {
"title": "Pre-réglages - {name}", "title": "Pre-réglages - {name}",
"description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques", "description": "Réglage des presets. Donnez la température cible (0 pour ignorer le preset)",
"data": { "data": {
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale" "eco_temp": "Preset Eco",
"comfort_temp": "Preset Comfort",
"boost_temp": "Preset Boost",
"frost_temp": "Preset Hors-gel",
"eco_ac_temp": "Preset Eco en mode AC",
"comfort_ac_temp": "Preset Comfort en mode AC",
"boost_ac_temp": "Preset Boost en mode AC",
"use_presets_central_config": "Utiliser la configuration centrale des presets"
},
"data_description": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost",
"frost_temp": "Température en preset Hors-gel",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC",
"use_presets_central_config": "Cochez pour utiliser la configuration centrale des presets. Décochez et saisissez les attributs pour utiliser une configuration des presets spécifique"
} }
}, },
"window": { "window": {
@@ -432,13 +427,28 @@
}, },
"presence": { "presence": {
"title": "Présence - {name}", "title": "Présence - {name}",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.", "description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"data": { "data": {
"presence_sensor_entity_id": "Capteur de présence", "presence_sensor_entity_id": "Capteur de présence",
"use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées" "eco_away_temp": "preset Eco",
"comfort_away_temp": "preset Comfort",
"boost_away_temp": "preset Boost",
"frost_away_temp": "preset Hors-gel",
"eco_ac_away_temp": "preset Eco en mode AC",
"comfort_ac_away_temp": "preset Comfort en mode AC",
"boost_ac_away_temp": "preset Boost en mode AC",
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence" "presence_sensor_entity_id": "Id d'entité du capteur de présence",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence",
"frost_away_temp": "Température en preset Hors-gel en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
} }
}, },
"advanced": { "advanced": {
@@ -541,53 +551,6 @@
} }
} }
} }
},
"number": {
"frost_temp": {
"name": "Hors gel "
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Confort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Hors gel clim"
},
"eco_ac_temp": {
"name": "Eco clim"
},
"comfort_ac_temp": {
"name": "Confort clim"
},
"boost_ac_temp": {
"name": "Boost clim"
},
"frost_away_temp": {
"name": "Hors gel abs"
},
"eco_away_temp": {
"name": "Eco abs"
},
"comfort_away_temp": {
"name": "Confort abs"
},
"boost_away_temp": {
"name": "Boost abs"
},
"eco_ac_away_temp": {
"name": "Eco clim abs"
},
"comfort_ac_away_temp": {
"name": "Confort clim abs"
},
"boost_ac_away_temp": {
"name": "Boost clim abs"
}
} }
} }
} }

View File

@@ -29,7 +29,6 @@
"heater_entity2_id": "Secondo riscaldatore", "heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato", "climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato", "climate_entity2_id": "Secondo termostato",
@@ -49,7 +48,6 @@
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_keep_alive": "Frequenza di aggiornamento dell'interruttore (facoltativo). Lasciare vuoto se non richiesto.",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato", "climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato", "climate_entity2_id": "Entity id del secondo termostato",
@@ -193,7 +191,6 @@
"heater_entity2_id": "Secondo riscaldatore", "heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato", "climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato", "climate_entity2_id": "Secondo termostato",
@@ -213,7 +210,6 @@
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_keep_alive": "Frequenza di aggiornamento dell'interruttore (facoltativo). Lasciare vuoto se non richiesto.",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato", "climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato", "climate_entity2_id": "Entity id del secondo termostato",

View File

@@ -32,7 +32,6 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from .const import UnknownEntity, overrides from .const import UnknownEntity, overrides
from .keep_alive import IntervalCaller
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -188,7 +187,6 @@ class UnderlyingSwitch(UnderlyingEntity):
thermostat: Any, thermostat: Any,
switch_entity_id: str, switch_entity_id: str,
initial_delay_sec: int, initial_delay_sec: int,
keep_alive_sec: int,
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -204,7 +202,6 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = 0 self._off_time_sec = 0
self._hvac_mode = None self._hvac_mode = None
self._keep_alive = IntervalCaller(hass, keep_alive_sec)
@property @property
def initial_delay_sec(self): def initial_delay_sec(self):
@@ -217,11 +214,6 @@ class UnderlyingSwitch(UnderlyingEntity):
"""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
def startup(self):
super().startup()
self._keep_alive.set_async_action(self._keep_alive_callback)
# @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 set_hvac_mode(self, hvac_mode: HVACMode) -> bool: async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change""" """Set the HVACmode. Returns true if something have change"""
@@ -245,43 +237,35 @@ class UnderlyingSwitch(UnderlyingEntity):
not self.is_inversed and real_state not self.is_inversed and real_state
) )
async def _keep_alive_callback(self):
"""Keep alive: Turn on if already turned on, turn off if already turned off."""
await (self.turn_on() if self.is_device_active else self.turn_off())
# @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."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_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:
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(domain, command, data) domain,
self._keep_alive.set_async_action(self._keep_alive_callback) command,
except Exception: data,
self._keep_alive.cancel() )
raise
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
async def turn_on(self): async def turn_on(self):
"""Turn heater toggleable device on.""" """Turn heater toggleable device on."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_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:
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(domain, command, data) domain,
self._keep_alive.set_async_action(self._keep_alive_callback) command,
except Exception: data,
self._keep_alive.cancel() )
raise
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
@@ -438,7 +422,6 @@ class UnderlyingSwitch(UnderlyingEntity):
def remove_entity(self): def remove_entity(self):
"""Remove the entity after stopping its cycle""" """Remove the entity after stopping its cycle"""
self._cancel_cycle() self._cancel_cycle()
self._keep_alive.cancel()
class UnderlyingClimate(UnderlyingEntity): class UnderlyingClimate(UnderlyingEntity):

View File

@@ -1,13 +1,8 @@
""" The API of Versatile Thermostat""" """ The API of Versatile Thermostat"""
import logging import logging
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.number import NumberEntity
from .const import ( from .const import (
DOMAIN, DOMAIN,
CONF_AUTO_REGULATION_EXPERT, CONF_AUTO_REGULATION_EXPERT,
@@ -56,24 +51,19 @@ class VersatileThermostatAPI(dict):
self._central_boiler_entity = None self._central_boiler_entity = None
self._threshold_number_entity = None self._threshold_number_entity = None
self._nb_active_number_entity = None self._nb_active_number_entity = None
self._central_configuration = None
# A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict()
def find_central_configuration(self): def find_central_configuration(self):
"""Search for a central configuration""" """Search for a central configuration"""
if not self._central_configuration: for config_entry in VersatileThermostatAPI._hass.config_entries.async_entries(
for ( DOMAIN
config_entry ):
) in VersatileThermostatAPI._hass.config_entries.async_entries(DOMAIN): if (
if ( config_entry.data.get(CONF_THERMOSTAT_TYPE)
config_entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
== CONF_THERMOSTAT_CENTRAL_CONFIG ):
): central_config = config_entry
self._central_configuration = config_entry return central_config
break return None
# return self._central_configuration
return self._central_configuration
def add_entry(self, entry: ConfigEntry): def add_entry(self, entry: ConfigEntry):
"""Add a new entry""" """Add a new entry"""
@@ -116,64 +106,10 @@ class VersatileThermostatAPI(dict):
): ):
"""register the two number entities needed for boiler activation""" """register the two number entities needed for boiler activation"""
self._threshold_number_entity = threshold_number_entity self._threshold_number_entity = threshold_number_entity
# If sensor and threshold number are initialized, reload the listener
# if self._nb_active_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_nb_device_active_boiler(self, nb_active_number_entity): def register_nb_device_active_boiler(self, nb_active_number_entity):
"""register the two number entities needed for boiler activation""" """register the two number entities needed for boiler activation"""
self._nb_active_number_entity = nb_active_number_entity self._nb_active_number_entity = nb_active_number_entity
# if self._threshold_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_temperature_number(
self,
config_id: str,
preset_name: str,
number_entity: NumberEntity,
):
"""Register the NumberEntity for a particular device / preset."""
# Search for device_name into the _number_temperatures dict
if not self._number_temperatures.get(config_id):
self._number_temperatures[config_id] = dict()
self._number_temperatures.get(config_id)[preset_name] = number_entity
def get_temperature_number_value(self, config_id, preset_name) -> float | None:
"""Returns the value of a previously registred NumberEntity which represent
a temperature. If no NumberEntity was previously registred, then returns None"""
entities = self._number_temperatures.get(config_id, None)
if entities:
entity = entities.get(preset_name, None)
if entity:
return entity.state
return None
async def init_vtherm_links(self, only_use_central=False):
"""INitialize all VTherms entities links
This method is called when HA is fully started (and all entities should be initialized)
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
"""
await self.reload_central_boiler_binary_listener()
await self.reload_central_boiler_entities_list()
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
if hasattr(entity, "init_presets"):
if (
only_use_central is False
or entity.use_central_config_temperature
):
await entity.init_presets(self.find_central_configuration())
async def reload_central_boiler_binary_listener(self):
"""Reloads the BinarySensor entity which listen to the number of
active devices and the thresholds entities"""
if self._central_boiler_entity:
await self._central_boiler_entity.listen_nb_active_vtherm_entity()
async def reload_central_boiler_entities_list(self): async def reload_central_boiler_entities_list(self):
"""Reload the central boiler list of entities if a central boiler is used""" """Reload the central boiler list of entities if a central boiler is used"""

View File

@@ -3,5 +3,5 @@
"content_in_root": false, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2024.3.1" "homeassistant": "2023.12.1"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

View File

@@ -1 +1,2 @@
homeassistant==2024.3.1 homeassistant==2023.12.1
ffmpeg

View File

@@ -25,9 +25,5 @@ fi
## without resulting to symlinks. ## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
## Link custom_components into config
# rm -f ${PWD}/config/custom_components
# ln -s ${PWD}/custom_components ${PWD}/config/
# Start Home Assistant # Start Home Assistant
hass --config "${PWD}/config" --debug hass --config "${PWD}/config" --debug

View File

@@ -1,9 +1,9 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Some common resources """ """ Some common resources """
import asyncio import asyncio
import logging import logging
from unittest.mock import patch, MagicMock # pylint: disable=unused-import from unittest.mock import patch, MagicMock
import pytest # pylint: disable=unused-import import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
@@ -23,7 +23,9 @@ from homeassistant.components.switch import (
SwitchEntity, SwitchEntity,
) )
from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN from homeassistant.components.number import (
NumberEntity,
)
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
@@ -70,12 +72,6 @@ from .const import ( # pylint: disable=unused-import
overrides, overrides,
) )
MOCK_FULL_FEATURES = {
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
}
FULL_SWITCH_CONFIG = ( FULL_SWITCH_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG MOCK_TH_OVER_SWITCH_USER_CONFIG
@@ -84,7 +80,6 @@ FULL_SWITCH_CONFIG = (
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG | MOCK_PRESETS_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG | MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG | MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG | MOCK_POWER_CONFIG
@@ -99,7 +94,6 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG | MOCK_PRESETS_AC_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG | MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG | MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG | MOCK_POWER_CONFIG
@@ -107,6 +101,7 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_ADVANCED_CONFIG | MOCK_ADVANCED_CONFIG
) )
PARTIAL_CLIMATE_CONFIG = ( PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
@@ -188,13 +183,12 @@ FULL_CENTRAL_CONFIG = {
CONF_NO_MOTION_PRESET: "frost", CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_power_sensor", CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor", CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
CONF_PRESET_POWER: 14, CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11, CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61, CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False, CONF_ADD_CENTRAL_BOILER_CONTROL: False,
} }
FULL_CENTRAL_CONFIG_WITH_BOILER = { FULL_CENTRAL_CONFIG_WITH_BOILER = {
@@ -235,7 +229,7 @@ FULL_CENTRAL_CONFIG_WITH_BOILER = {
CONF_SECURITY_DELAY_MIN: 61, CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: True, CONF_ADD_CENTRAL_BOILER_CONTROL: True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on", CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off", CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
} }
@@ -499,18 +493,14 @@ async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> BaseThermostat: ) -> BaseThermostat:
"""Creates and return a TPI Thermostat""" """Creates and return a TPI Thermostat"""
entry.add_to_hass(hass) with patch(
await hass.config_entries.async_setup(entry.entry_id) "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
assert entry.state is ConfigEntryState.LOADED ):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
# We should reload the VTherm links return search_entity(hass, entity_id, CLIMATE_DOMAIN)
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
# central_config = vtherm_api.find_central_configuration()
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
# if entity and hasattr(entity, "init_presets")::
# await entity.init_presets(central_config)
return entity
async def create_central_config( # pylint: disable=dangerous-default-value async def create_central_config( # pylint: disable=dangerous-default-value
@@ -533,14 +523,11 @@ async def create_central_config( # pylint: disable=dangerous-default-value
central_configuration = api.find_central_configuration() central_configuration = api.find_central_configuration()
assert central_configuration is not None assert central_configuration is not None
return central_configuration
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity: def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain""" """Search and return the entity in the domain"""
component = hass.data[domain] component = hass.data[domain]
for entity in component.entities: for entity in component.entities:
_LOGGER.debug("Found %s entity: %s", domain, entity.entity_id)
if entity.entity_id == entity_id: if entity.entity_id == entity_id:
return entity return entity
return None return None
@@ -860,25 +847,3 @@ def cancel_switchs_cycles(entity: BaseThermostat):
return return
for under in entity._underlyings: for under in entity._underlyings:
under._cancel_cycle() under._cancel_cycle()
async def set_climate_preset_temp(
entity: BaseThermostat, temp_number_name: str, temp: float
):
"""Set a preset value in the temp Number entity"""
number_entity_id = (
NUMBER_DOMAIN
+ "."
+ entity.entity_id.split(".")[1]
+ "_preset_"
+ temp_number_name
+ PRESET_TEMP_SUFFIX
)
temp_entity = search_entity(
entity.hass,
number_entity_id,
NUMBER_DOMAIN,
)
if temp_entity:
await temp_entity.async_set_native_value(temp)

View File

@@ -35,31 +35,8 @@ from .commons import (
FULL_CENTRAL_CONFIG_WITH_BOILER, FULL_CENTRAL_CONFIG_WITH_BOILER,
) )
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
# ...
# ...
def pytest_runtest_setup():
"""setup tests"""
socket_allow_hosts(
allowed=["localhost", "127.0.0.1", "::1"], allow_unix_socket=True
)
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
# Permet d'exclure certains test en mode d'ex
# sequential = pytest.mark.sequential
# This fixture allow to execute some tests first and not in //
# @pytest.fixture
# def order():
# return 1
#
# This fixture enables loading custom integrations in all tests. # This fixture enables loading custom integrations in all tests.
# Remove to enable selective use of this fixture # Remove to enable selective use of this fixture

View File

@@ -1,5 +1,4 @@
""" The commons const for all tests """ """ The commons const for all tests """
from homeassistant.components.climate.const import ( # pylint: disable=unused-import from homeassistant.components.climate.const import ( # pylint: disable=unused-import
PRESET_BOOST, PRESET_BOOST,
PRESET_COMFORT, PRESET_COMFORT,
@@ -19,10 +18,10 @@ MOCK_TH_OVER_SWITCH_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_WINDOW_FEATURE: True, CONF_USE_WINDOW_FEATURE: True,
# CONF_USE_MOTION_FEATURE: True, CONF_USE_MOTION_FEATURE: True,
# CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
# CONF_USE_PRESENCE_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
} }
@@ -53,7 +52,7 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
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, CONF_USE_CENTRAL_MODE: True
# Keep default values which are False # Keep default values which are False
} }
@@ -75,7 +74,6 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch", CONF_HEATER: "switch.mock_switch",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
@@ -93,7 +91,6 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER_2: "switch.mock_4switch1", CONF_HEATER_2: "switch.mock_4switch1",
CONF_HEATER_3: "switch.mock_4switch2", CONF_HEATER_3: "switch.mock_4switch2",
CONF_HEATER_4: "switch.mock_4switch3", CONF_HEATER_4: "switch.mock_4switch3",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
@@ -138,23 +135,21 @@ MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
CONF_AUTO_REGULATION_PERIOD_MIN: 1, CONF_AUTO_REGULATION_PERIOD_MIN: 1,
} }
# TODO remove this later
MOCK_PRESETS_CONFIG = { MOCK_PRESETS_CONFIG = {
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7, PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 16, PRESET_ECO + "_temp": 16,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 17, PRESET_COMFORT + "_temp": 17,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 18, PRESET_BOOST + "_temp": 18,
} }
# TODO remove this later
MOCK_PRESETS_AC_CONFIG = { MOCK_PRESETS_AC_CONFIG = {
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7, PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17, PRESET_ECO + "_temp": 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19, PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 20, PRESET_BOOST + "_temp": 20,
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 25, PRESET_ECO + "_ac_temp": 25,
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 23, PRESET_COMFORT + "_ac_temp": 23,
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 21, PRESET_BOOST + "_ac_temp": 21,
} }
MOCK_WINDOW_CONFIG = { MOCK_WINDOW_CONFIG = {
@@ -190,10 +185,20 @@ MOCK_POWER_CONFIG = {
MOCK_PRESENCE_CONFIG = { MOCK_PRESENCE_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor", CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
} }
MOCK_PRESENCE_AC_CONFIG = { MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor", CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
} }
MOCK_ADVANCED_CONFIG = { MOCK_ADVANCED_CONFIG = {

View File

@@ -211,14 +211,13 @@ async def test_over_climate_auto_fan_mode_turbo_activation(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
): ):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# entity: ThermostatOverClimate = search_entity(
# entity: ThermostatOverClimate = search_entity( hass, "climate.theoverclimatemockname", "climate"
# hass, "climate.theoverclimatemockname", "climate" )
# )
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)

View File

@@ -52,19 +52,18 @@ async def test_over_climate_regulation(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
): ):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -128,7 +127,9 @@ async def test_over_climate_regulation(
# the regulated temperature should be under # the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 18 - 2.5 assert (
entity.regulated_target_temp == 18 - 2
) # normally 0.6 but round_to_nearest gives 0.5
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -162,19 +163,18 @@ async def test_over_climate_regulation_ac_mode(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
): ):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -379,9 +379,6 @@ async def test_over_climate_regulation_limitations(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# Disable this test which is not working when run in // of others.
# I couldn't find out why
@pytest.mark.skip
async def test_over_climate_regulation_use_device_temp( async def test_over_climate_regulation_use_device_temp(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
): ):
@@ -392,7 +389,7 @@ async def test_over_climate_regulation_use_device_temp(
title="TheOverClimateMockName", title="TheOverClimateMockName",
unique_id="uniqueId", unique_id="uniqueId",
# This is include a medium regulation # This is include a medium regulation
data=PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP | {CONF_AUTO_REGULATION_DTEMP: 0.5}, data=PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP,
) )
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
@@ -480,7 +477,7 @@ async def test_over_climate_regulation_use_device_temp(
# room temp is 15 # room temp is 15
# target is 18 # target is 18
# internal heater temp is 20 # internal heater temp is 20
fake_underlying_climate.set_current_temperature(20.1) fake_underlying_climate.set_current_temperature(20)
await entity.async_set_temperature(temperature=18) await entity.async_set_temperature(temperature=18)
await send_ext_temperature_change_event(entity, 9, event_timestamp) await send_ext_temperature_change_event(entity, 9, event_timestamp)
@@ -493,7 +490,7 @@ async def test_over_climate_regulation_use_device_temp(
# the regulated temperature should be under (device offset is -2) # the regulated temperature should be under (device offset is -2)
assert entity.regulated_target_temp > entity.target_temperature assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 19.5 # round(18 + 1.4, 0.5) assert entity.regulated_target_temp == 19.4 # 18 + 1.4
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
@@ -502,7 +499,7 @@ async def test_over_climate_regulation_use_device_temp(
"set_temperature", "set_temperature",
{ {
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 24.5, # round(19.5 + 5, 0.5) "temperature": 24.4, # 19.4 + 5
"target_temp_high": 30, "target_temp_high": 30,
"target_temp_low": 15, "target_temp_low": 15,
}, },
@@ -516,7 +513,7 @@ async def test_over_climate_regulation_use_device_temp(
# internal heater temp is 27 # internal heater temp is 27
await entity.async_set_hvac_mode(HVACMode.COOL) await entity.async_set_hvac_mode(HVACMode.COOL)
await entity.async_set_temperature(temperature=23) await entity.async_set_temperature(temperature=23)
fake_underlying_climate.set_current_temperature(26.9) fake_underlying_climate.set_current_temperature(27)
await send_ext_temperature_change_event(entity, 30, event_timestamp) await send_ext_temperature_change_event(entity, 30, event_timestamp)
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
@@ -526,9 +523,9 @@ async def test_over_climate_regulation_use_device_temp(
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(entity, 25, event_timestamp) await send_temperature_change_event(entity, 25, event_timestamp)
# the regulated temperature should be upper (device offset is +1.9) # the regulated temperature should be upper (device offset is +2)
assert entity.regulated_target_temp < entity.target_temperature assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 22.5 assert entity.regulated_target_temp == 22.9
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
@@ -537,7 +534,7 @@ async def test_over_climate_regulation_use_device_temp(
"set_temperature", "set_temperature",
{ {
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 24.5, # round(22.5 + 1.9° of offset) "temperature": 24.9, # 22.9 + 2° of offset
"target_temp_high": 30, "target_temp_high": 30,
"target_temp_low": 15, "target_temp_low": 15,
}, },

View File

@@ -380,19 +380,18 @@ async def test_bug_82(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
) as mock_find_climate: ) as mock_find_climate:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity = find_my_entity("climate.theoverclimatemockname")
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
@@ -491,19 +490,18 @@ async def test_bug_101(
) as mock_find_climate, patch( ) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode: ) as mock_underlying_set_hvac_mode:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity = find_my_entity("climate.theoverclimatemockname")
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
@@ -608,19 +606,18 @@ async def test_bug_272(
), patch( ), patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call: ) as mock_service_call:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity = find_my_entity("climate.theoverclimatemockname")
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity

View File

@@ -156,29 +156,23 @@ async def test_update_central_boiler_state_simple(
await switch1.async_turn_on() await switch1.async_turn_on()
switch1.async_write_ha_state() switch1.async_write_ha_state()
# Wait for state event propagation # Wait for state event propagation
await asyncio.sleep(1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 2 assert mock_service_call.call_count >= 1
# Sometimes this test fails # Sometimes this test fails
mock_service_call.assert_has_calls( # mock_service_call.assert_has_calls(
[ # [
call.service_call( # call.service_call(
"switch", # "switch",
"turn_on", # "turn_on",
{"entity_id": "switch.switch1"}, # service_data={},
), # target={"entity_id": "switch.pompe_chaudiere"},
call( # ),
"switch", # ]
"turn_on", # )
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
],
any_order=True,
)
assert mock_send_event.call_count >= 1 assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls( mock_send_event.assert_has_calls(

View File

@@ -4,13 +4,23 @@
from unittest.mock import patch # , call from unittest.mock import patch # , call
# from datetime import datetime # , timedelta # from datetime import datetime # , timedelta
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.config_entries import SOURCE_USER # from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState, SOURCE_USER
# from homeassistant.helpers.entity_component import EntityComponent
# from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import ( from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch, ThermostatOverSwitch,
) )
@@ -21,8 +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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [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(
@@ -66,20 +76,17 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
CONF_SECURITY_DELAY_MIN: 61, CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False, CONF_ADD_CENTRAL_BOILER_CONTROL: False,
}, },
) )
entity = await create_thermostat( central_config_entry.add_to_hass(hass)
hass, central_config_entry, "climate.thecentralconfigmockname" await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
) )
# central_config_entry.add_to_hass(hass)
# await hass.config_entries.async_setup(central_config_entry.entry_id)
# assert central_config_entry.state is ConfigEntryState.LOADED
#
# entity: ThermostatOverClimate = search_entity(
# hass, "climate.thecentralconfigmockname", "climate"
# )
assert entity is None assert entity is None
@@ -94,8 +101,8 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
assert api.nb_active_device_for_boiler_entity is None assert api.nb_active_device_for_boiler_entity is None
assert api.nb_active_device_for_boiler is None assert api.nb_active_device_for_boiler is None
assert api.nb_active_device_for_boiler_threshold_entity is not None assert api.nb_active_device_for_boiler_threshold_entity is None
assert api.nb_active_device_for_boiler_threshold == 1 # the default value assert api.nb_active_device_for_boiler_threshold is None
# @pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -178,8 +185,8 @@ async def test_minimal_over_switch_wo_central_config(
entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) # @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_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
): ):
@@ -296,8 +303,8 @@ async def test_full_over_switch_wo_central_config(
entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) # @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_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
): ):
@@ -430,13 +437,7 @@ async def test_over_switch_with_central_config_but_no_central_config(
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "menu"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "main"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result["errors"] == {}
@@ -447,71 +448,15 @@ async def test_over_switch_with_central_config_but_no_central_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_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
}, },
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# in case of error we stays in main # in case of error we stays in main
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {"use_main_central_config": "no_central_config"} assert result["errors"] == {"use_main_central_config": "no_central_config"}
async def test_migration_of_central_config(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
version=CONFIG_VERSION,
# An old minor version
minor_version=1,
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
# The old central_boiler parameter
"add_central_boiler_control": True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
},
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
)
assert entity is None
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
assert central_config_entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
# Test that VTherm API have any central boiler entities
# It should have been migrated and initialized
assert api.nb_active_device_for_boiler_entity is not None
assert api.nb_active_device_for_boiler == 0
assert api.nb_active_device_for_boiler_threshold_entity is not None
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1

View File

@@ -23,7 +23,7 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# @pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) # @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_config_with_central_mode_true( async def test_config_with_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state hass: HomeAssistant, skip_hass_states_is_state
): ):
@@ -170,8 +170,6 @@ async def test_config_with_central_mode_none(
assert entity.last_central_mode is None # cause no central config exists assert entity.last_central_mode is None # cause no central config exists
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true( async def test_switch_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -312,8 +310,6 @@ async def test_switch_change_central_mode_true(
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_ac_change_central_mode_true( async def test_switch_ac_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -448,8 +444,6 @@ async def test_switch_ac_change_central_mode_true(
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_climate_ac_change_central_mode_false( async def test_climate_ac_change_central_mode_false(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -583,8 +577,6 @@ async def test_climate_ac_change_central_mode_false(
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_climate_ac_only_change_central_mode_true( async def test_climate_ac_only_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -742,8 +734,6 @@ async def test_climate_ac_only_change_central_mode_true(
assert entity.preset_mode == PRESET_ECO assert entity.preset_mode == PRESET_ECO
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true_with_window( async def test_switch_change_central_mode_true_with_window(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -899,8 +889,6 @@ async def test_switch_change_central_mode_true_with_window(
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true_with_cool_only_and_window( async def test_switch_change_central_mode_true_with_cool_only_and_window(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):

View File

@@ -2,7 +2,6 @@
""" Test the Versatile Thermostat config flow """ """ Test the Versatile Thermostat config flow """
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.config_entries import SOURCE_USER, ConfigEntry
@@ -20,14 +19,14 @@ async def test_show_form(hass: HomeAssistant, init_vtherm_api) -> None:
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# Disable this test which don't work anymore (kill the pytest !) # Disable this test which don't work anymore (kill the pytest !)
# @pytest.mark.skip @pytest.mark.skip
async def test_user_config_flow_over_switch( async def test_user_config_flow_over_switch(
hass: HomeAssistant, skip_hass_states_get, init_central_config hass: HomeAssistant, skip_hass_states_get, init_central_config
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
@@ -35,147 +34,49 @@ async def test_user_config_flow_over_switch(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
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_TH_OVER_SWITCH_USER_CONFIG
user_input={
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
]
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "main"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result.get("errors") == {} assert result["errors"] == {}
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_TH_OVER_SWITCH_MAIN_CONFIG
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "type"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result.get("errors") == {} assert result["errors"] == {}
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_TH_OVER_SWITCH_TYPE_CONFIG
user_input={
CONF_HEATER: "switch.mock_switch",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"tpi",
"presets",
"advanced",
"finalize", # because by default all options are "use central config"
]
assert result.get("errors") is None assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "tpi"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "presets"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "features"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "features"
assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_WINDOW_FEATURE: True,
},
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["menu_options"] == [
"main",
"features",
"type",
"tpi",
"presets",
"window",
"motion",
"power",
"presence",
"advanced",
"configuration_not_complete",
# "finalize" : because for motion we need an motion sensor
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "window"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -184,16 +85,10 @@ async def test_user_config_flow_over_switch(
CONF_USE_WINDOW_CENTRAL_CONFIG: True, CONF_USE_WINDOW_CENTRAL_CONFIG: True,
}, },
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "motion"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "motion" assert result["step_id"] == "motion"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -203,74 +98,42 @@ async def test_user_config_flow_over_switch(
}, },
) )
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "power"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "power" assert result["step_id"] == "power"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_POWER_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_POWER_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"tpi",
"presets",
"window",
"motion",
"power",
"presence",
"advanced",
"finalize",
]
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "presence"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "presence" assert result["step_id"] == "presence"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
CONF_PRESENCE_SENSOR: "person.presence_sensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: True, CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "advanced"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
) )
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
result["flow_id"], user_input={"next_step_id": "finalize"}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result.get("errors") is None
assert result["data"] == ( assert result["data"] == (
MOCK_TH_OVER_SWITCH_USER_CONFIG MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_MAIN_CONFIG | MOCK_TH_OVER_SWITCH_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"} | {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"}
| {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"} | {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"}
# | {CONF_PRESENCE_SENSOR: "person.presence_sensor"} now in central config | {CONF_PRESENCE_SENSOR: "person.presence_sensor"}
| { | {
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True, CONF_USE_TPI_CENTRAL_CONFIG: True,
@@ -282,11 +145,6 @@ async def test_user_config_flow_over_switch(
CONF_USE_ADVANCED_CENTRAL_CONFIG: True, CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: True, CONF_USE_CENTRAL_MODE: True,
CONF_USED_BY_CENTRAL_BOILER: False, CONF_USED_BY_CENTRAL_BOILER: False,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
} }
) )
assert result["result"] assert result["result"]
@@ -298,209 +156,86 @@ async def test_user_config_flow_over_switch(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO this test fails when run in // but works alone
@pytest.mark.skip
async def test_user_config_flow_over_climate( async def test_user_config_flow_over_climate(
hass: HomeAssistant, skip_hass_states_get hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_switch features and never use central config. """Test the config flow with all thermostat_over_climate features and no additional features"""
We don't use any features""" await create_central_config(hass)
# await create_central_config(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
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_TH_OVER_CLIMATE_USER_CONFIG
user_input={
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
]
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "main"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result.get("errors") == {} assert result["errors"] == {}
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_TH_OVER_CLIMATE_MAIN_CONFIG
user_input={
CONF_NAME: "TheOverClimateMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: True,
# Keep default values which are False
},
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result.get("errors") == {} assert result["errors"] == {}
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_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
user_input={
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
# Keep default values which are False
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "type"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result.get("errors") == {} assert result["errors"] == {}
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_TH_OVER_CLIMATE_TYPE_CONFIG
user_input={
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
# "finalize", # because we need Advanced default parameters
]
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"], user_input={"next_step_id": "presets"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result.get("errors") is None assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "features"} result["flow_id"], user_input=MOCK_PRESETS_CONFIG
) )
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "features"
assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result["flow_id"],
user_input={
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_WINDOW_FEATURE: False,
},
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
# "finalize", finalize is not present waiting for advanced configuration
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "advanced"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result.get("errors") == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False}
user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False},
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result.get("errors") == {} assert result["errors"] == {}
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_ADVANCED_CONFIG
user_input={
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"finalize", # Now finalize is present
]
result = await hass.config_entries.flow.async_configure( assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
result["flow_id"], user_input={"next_step_id": "finalize"}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result.get("errors") is None
assert result[ assert result[
"data" "data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | { ] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | MOCK_DEFAULT_FEATURE_CONFIG | {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
} | MOCK_DEFAULT_FEATURE_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: False, CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False, CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False, CONF_USE_PRESETS_CENTRAL_CONFIG: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_WINDOW_CENTRAL_CONFIG: False, CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False, CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False, CONF_USE_POWER_CENTRAL_CONFIG: False,
@@ -517,8 +252,6 @@ async def test_user_config_flow_over_climate(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO reimplement this
@pytest.mark.skip
async def test_user_config_flow_window_auto_ok( async def test_user_config_flow_window_auto_ok(
hass: HomeAssistant, hass: HomeAssistant,
skip_hass_states_get, skip_hass_states_get,
@@ -531,7 +264,7 @@ async def test_user_config_flow_window_auto_ok(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -541,9 +274,9 @@ async def test_user_config_flow_window_auto_ok(
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -561,59 +294,59 @@ async def test_user_config_flow_window_auto_ok(
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False}, user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False},
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result.get("errors") is None assert result["errors"] == {}
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_AUTO_CONFIG,
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
@@ -655,8 +388,6 @@ async def test_user_config_flow_window_auto_ok(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO reimplement this
@pytest.mark.skip
async def test_user_config_flow_window_auto_ko( async def test_user_config_flow_window_auto_ko(
hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument
): ):
@@ -668,7 +399,7 @@ async def test_user_config_flow_window_auto_ko(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -678,9 +409,9 @@ async def test_user_config_flow_window_auto_ko(
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -697,41 +428,41 @@ async def test_user_config_flow_window_auto_ko(
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -741,9 +472,9 @@ async def test_user_config_flow_window_auto_ko(
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -752,9 +483,9 @@ async def test_user_config_flow_window_auto_ko(
# Since issue #280 we cannot have the error because we only display the # Since issue #280 we cannot have the error because we only display the
# MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured # MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured
assert result["type"] == FlowResultType.MENU 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.get("errors") is None assert result["errors"] == {}
# "window_sensor_entity_id": "window_open_detection_method" # "window_sensor_entity_id": "window_open_detection_method"
# } # }
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
@@ -762,8 +493,6 @@ async def test_user_config_flow_window_auto_ko(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO reimplement this
@pytest.mark.skip
async def test_user_config_flow_over_4_switches( async def test_user_config_flow_over_4_switches(
hass: HomeAssistant, hass: HomeAssistant,
skip_hass_states_get, skip_hass_states_get,
@@ -773,11 +502,11 @@ async def test_user_config_flow_over_4_switches(
await create_central_config(hass) await create_central_config(hass)
SOURCE_CONFIG = { # pylint: disable=invalid-name SOURCE_CONFIG = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
} }
MAIN_CONFIG = { # pylint: disable=invalid-name MAIN_CONFIG = { # pylint: disable=wildcard-import, invalid-name
CONF_NAME: "TheOver4SwitchMockName", CONF_NAME: "TheOver4SwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
@@ -791,12 +520,11 @@ async def test_user_config_flow_over_4_switches(
CONF_USED_BY_CENTRAL_BOILER: False, CONF_USED_BY_CENTRAL_BOILER: False,
} }
TYPE_CONFIG = { # pylint: disable=invalid-name TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
CONF_HEATER: "switch.mock_switch1", CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2", CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
@@ -806,7 +534,7 @@ async def test_user_config_flow_over_4_switches(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -814,43 +542,43 @@ async def test_user_config_flow_over_4_switches(
user_input=SOURCE_CONFIG, user_input=SOURCE_CONFIG,
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=MAIN_CONFIG, user_input=MAIN_CONFIG,
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=TYPE_CONFIG, user_input=TYPE_CONFIG,
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result.get("errors") is None assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}

View File

@@ -260,7 +260,6 @@ async def test_multiple_switchs(
CONF_HEATER_2: "switch.mock_switch2", CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -747,7 +746,6 @@ async def test_multiple_switch_power_management(
CONF_HEATER_2: "switch.mock_switch2", CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,

View File

@@ -42,15 +42,15 @@ def test_pi_algorithm_basics():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # error change sign assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9 assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.0 assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2 assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20 # = assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # =
def test_pi_algorithm_light(): def test_pi_algorithm_light():
@@ -78,15 +78,15 @@ def test_pi_algorithm_light():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # Error sign change assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9 assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21 assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8 assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5 assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2 assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # = assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # =
def test_pi_algorithm_medium(): def test_pi_algorithm_medium():
@@ -114,20 +114,20 @@ def test_pi_algorithm_medium():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.4 assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.4
assert the_algo.calculate_regulated_temperature(19, 10) == 22.3 assert the_algo.calculate_regulated_temperature(19, 10) == 22.3
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9 assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
assert the_algo.calculate_regulated_temperature(21, 10) == 21.0 # error sign change assert the_algo.calculate_regulated_temperature(21, 10) == 21.4
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 assert the_algo.calculate_regulated_temperature(21, 10) == 21.3
assert the_algo.calculate_regulated_temperature(20, 10) == 21.1 assert the_algo.calculate_regulated_temperature(20, 10) == 21.7
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 21.3 assert the_algo.calculate_regulated_temperature(20, 8) == 21.9
assert the_algo.calculate_regulated_temperature(20, 6) == 21.5 assert the_algo.calculate_regulated_temperature(20, 6) == 22.1
assert the_algo.calculate_regulated_temperature(20, 4) == 21.7 assert the_algo.calculate_regulated_temperature(20, 4) == 22.3
assert the_algo.calculate_regulated_temperature(20, 2) == 21.9 assert the_algo.calculate_regulated_temperature(20, 2) == 22.5
assert the_algo.calculate_regulated_temperature(20, 0) == 22.1 assert the_algo.calculate_regulated_temperature(20, 0) == 22.7
assert the_algo.calculate_regulated_temperature(20, -2) == 22.3 assert the_algo.calculate_regulated_temperature(20, -2) == 22.9
assert the_algo.calculate_regulated_temperature(20, -4) == 22.5 assert the_algo.calculate_regulated_temperature(20, -4) == 23.0
assert the_algo.calculate_regulated_temperature(20, -6) == 22.7 assert the_algo.calculate_regulated_temperature(20, -6) == 23.0
assert the_algo.calculate_regulated_temperature(20, -8) == 22.9 assert the_algo.calculate_regulated_temperature(20, -8) == 23.0
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
@@ -173,22 +173,22 @@ def test_pi_algorithm_strong():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24 assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9 assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
assert the_algo.calculate_regulated_temperature(21, 10) == 22.3 # error sign change assert the_algo.calculate_regulated_temperature(21, 10) == 23.3
assert the_algo.calculate_regulated_temperature(21, 10) == 21.8 assert the_algo.calculate_regulated_temperature(21, 10) == 23.1
assert the_algo.calculate_regulated_temperature(21, 10) == 21.5 assert the_algo.calculate_regulated_temperature(21, 10) == 22.9
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 assert the_algo.calculate_regulated_temperature(21, 10) == 22.7
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 assert the_algo.calculate_regulated_temperature(21, 10) == 22.5
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9 assert the_algo.calculate_regulated_temperature(21, 10) == 22.3
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 assert the_algo.calculate_regulated_temperature(21, 10) == 22.1
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 21.5 assert the_algo.calculate_regulated_temperature(20, 8) == 22.9
assert the_algo.calculate_regulated_temperature(20, 6) == 21.9 assert the_algo.calculate_regulated_temperature(20, 6) == 23.3
assert the_algo.calculate_regulated_temperature(20, 4) == 22.3 assert the_algo.calculate_regulated_temperature(20, 4) == 23.7
assert the_algo.calculate_regulated_temperature(20, 2) == 22.7 assert the_algo.calculate_regulated_temperature(20, 2) == 24
assert the_algo.calculate_regulated_temperature(20, 0) == 23.1 assert the_algo.calculate_regulated_temperature(20, 0) == 24
assert the_algo.calculate_regulated_temperature(20, -2) == 23.5 assert the_algo.calculate_regulated_temperature(20, -2) == 24
assert the_algo.calculate_regulated_temperature(20, -4) == 23.9 assert the_algo.calculate_regulated_temperature(20, -4) == 24
assert the_algo.calculate_regulated_temperature(20, -6) == 24 assert the_algo.calculate_regulated_temperature(20, -6) == 24
assert the_algo.calculate_regulated_temperature(20, -8) == 24 assert the_algo.calculate_regulated_temperature(20, -8) == 24

View File

@@ -5,6 +5,10 @@ from unittest.mock import patch, call
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
@@ -34,7 +38,18 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
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: ) as mock_send_event:
entity = await create_thermostat(hass, entry, "climate.theoverswitchmockname") 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: BaseThermostat = find_my_entity("climate.theoverswitchmockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverSwitch) assert isinstance(entity, ThermostatOverSwitch)
@@ -93,19 +108,18 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
) as mock_find_climate: ) as mock_find_climate:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity = find_my_entity("climate.theoverclimatemockname")
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -160,24 +174,23 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
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: ) as mock_send_event:
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname") entry.add_to_hass(hass)
# entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)
# await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED
# assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity:
# def find_my_entity(entity_id) -> ClimateEntity: """Find my new entity"""
# """Find my new entity""" component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] for entity in component.entities:
# for entity in component.entities: if entity.entity_id == entity_id:
# if entity.entity_id == entity_id: return entity
# return entity
# entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
# entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
assert entity assert entity
assert entity.name == "TheOver4SwitchMockName" assert entity.name == "TheOver4SwitchMockName"
assert entity.is_over_switch assert entity.is_over_climate is False
assert entity.hvac_action is HVACAction.OFF assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp assert entity.target_temperature == entity.min_temp
@@ -251,7 +264,6 @@ async def test_over_switch_deactivate_preset(
CONF_HEATER_2: None, CONF_HEATER_2: None,
CONF_HEATER_3: None, CONF_HEATER_3: None,
CONF_HEATER_4: None, CONF_HEATER_4: None,
CONF_HEATER_KEEP_ALIVE: 0,
CONF_SECURITY_DELAY_MIN: 10, CONF_SECURITY_DELAY_MIN: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 10, CONF_MINIMAL_ACTIVATION_DELAY: 10,
}, },

View File

@@ -56,23 +56,6 @@ async def test_over_switch_ac_full_start(
assert entity assert entity
assert isinstance(entity, ThermostatOverSwitch) assert isinstance(entity, ThermostatOverSwitch)
# Initialise the preset temp
await set_climate_preset_temp(
entity, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, 7
)
await set_climate_preset_temp(entity, PRESET_ECO + PRESET_AWAY_SUFFIX, 16)
await set_climate_preset_temp(entity, PRESET_COMFORT + PRESET_AWAY_SUFFIX, 17)
await set_climate_preset_temp(entity, PRESET_BOOST + PRESET_AWAY_SUFFIX, 18)
await set_climate_preset_temp(
entity, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 27
)
await set_climate_preset_temp(
entity, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 26
)
await set_climate_preset_temp(
entity, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 25
)
assert entity.name == "TheOverSwitchMockName" assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_climate is False # pylint: disable=protected-access assert entity.is_over_climate is False # pylint: disable=protected-access
assert entity.ac_mode is True assert entity.ac_mode is True

View File

@@ -1,258 +0,0 @@
"""Test the switch keep-alive feature."""
import logging
from collections.abc import AsyncGenerator, Callable, Awaitable
from dataclasses import dataclass
from unittest.mock import ANY, _Call, call, patch
from datetime import datetime, timedelta
from typing import cast
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.fixture
def config_entry() -> MockConfigEntry:
"""Return common test data"""
return MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_HEATER_KEEP_ALIVE: 1,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.1,
},
)
@dataclass
class CommonMocks:
"""Common mocked objects used by most test cases"""
config_entry: MockConfigEntry
hass: HomeAssistant
thermostat: ThermostatOverSwitch
mock_is_state: MagicMock
mock_service_call: MagicMock
mock_async_track_time_interval: MagicMock
mock_send_event: MagicMock
# pylint: disable=redefined-outer-name, line-too-long, protected-access
@pytest.fixture
async def common_mocks(
config_entry: MockConfigEntry,
hass: HomeAssistant,
) -> AsyncGenerator[CommonMocks, None]:
"""Create and destroy a ThermostatOverSwitch as a test fixture"""
# fmt: off
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call, \
patch("homeassistant.core.StateMachine.is_state", return_value=False) as mock_is_state, \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.keep_alive.async_track_time_interval") as mock_async_track_time_interval:
# fmt: on
thermostat = cast(ThermostatOverSwitch, await create_thermostat(
hass, config_entry, "climate.theoverswitchmockname"
))
yield CommonMocks(
config_entry=config_entry,
hass=hass,
thermostat=thermostat,
mock_is_state=mock_is_state,
mock_service_call=mock_service_call,
mock_async_track_time_interval=mock_async_track_time_interval,
mock_send_event=mock_send_event,
)
# Clean the entity
thermostat.remove_thermostat()
class TestKeepAlive:
"""Tests for the switch keep-alive feature"""
# pylint: disable=attribute-defined-outside-init
def setup_method(self):
"""Initialise test case data before the execution of each test case method."""
self._prev_service_calls: list[_Call] = []
self._prev_atti_call_count = 0 # atti: async_time_track_interval
self._prev_atti_callback: Callable[[datetime], Awaitable[None]] | None = None
def _assert_service_call(
self, cm: CommonMocks, expected_additional_calls: list[_Call]
):
"""Assert that hass.services.async_call() was called with the expected arguments,
cumulatively over the course of long test cases."""
self._prev_service_calls.extend(expected_additional_calls)
cm.mock_service_call.assert_has_calls(self._prev_service_calls)
def _assert_async_mock_track_time_interval(
self, cm: CommonMocks, expected_additional_calls: int
):
"""Assert that async_track_time_interval() was called the expected number of times
with the expected arguments, cumulatively over the course of long test cases."""
self._prev_atti_call_count += expected_additional_calls
assert (
cm.mock_async_track_time_interval.call_count == self._prev_atti_call_count
)
interval = timedelta(seconds=cm.config_entry.data[CONF_HEATER_KEEP_ALIVE])
cm.mock_async_track_time_interval.assert_called_with(cm.hass, ANY, interval)
keep_alive_callback = cm.mock_async_track_time_interval.call_args.args[1]
assert callable(keep_alive_callback)
self._prev_atti_callback = keep_alive_callback
async def _assert_multipe_keep_alive_callback_calls(
self, cm: CommonMocks, n_calls: int
):
"""Call the keep-alive callback a few times as if `async_track_time_interval()` had
done it, and assert that this triggers further calls to `async_track_time_interval()`.
"""
old_callback = self._prev_atti_callback
assert (
old_callback
), "The keep-alive callback should have been called before, but it wasn't."
interval = timedelta(seconds=cm.config_entry.data[CONF_HEATER_KEEP_ALIVE])
for _ in range(n_calls):
await old_callback(datetime.fromtimestamp(0))
self._prev_atti_call_count += 1
assert (
cm.mock_async_track_time_interval.call_count
== self._prev_atti_call_count
)
cm.mock_async_track_time_interval.assert_called_with(cm.hass, ANY, interval)
new_callback = cm.mock_async_track_time_interval.call_args.args[1]
assert new_callback is not old_callback
assert new_callback.__qualname__ == old_callback.__qualname__
old_callback = new_callback
self._prev_atti_callback = old_callback
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_keep_alive_startup(self, common_mocks: CommonMocks):
"""Test that switch keep-alive service calls are made at startup time."""
thermostat = common_mocks.thermostat
await thermostat.async_set_hvac_mode(HVACMode.HEAT)
assert thermostat.hvac_mode is HVACMode.HEAT
assert thermostat.target_temperature == 15
assert thermostat.is_device_active is False
# When the keep-alive feature is enabled, regular calls to the switch
# turn_on / turn_off methods are _scheduled_ at start up.
self._assert_async_mock_track_time_interval(common_mocks, 1)
# Those keep-alive calls are scheduled but until the callback is called,
# no service calls are made to the SERVICE_TURN_OFF home assistant service.
self._assert_service_call(common_mocks, [])
# Call the keep-alive callback a few times (as if `async_track_time_interval`
# had done it) and assert that the callback function is replaced each time.
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
# Every time the keep-alive callback is called, the home assistant switch
# turn on/off service should be called too.
self._assert_service_call(
common_mocks,
[
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
],
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_keep_alive(self, common_mocks: CommonMocks):
"""Test that switch keep-alive service calls are made during thermostat operation."""
hass = common_mocks.hass
thermostat = common_mocks.thermostat
await thermostat.async_set_hvac_mode(HVACMode.HEAT)
assert thermostat.hvac_mode is HVACMode.HEAT
assert thermostat.target_temperature == 15
assert thermostat.is_device_active is False
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=4)
# 1. Decrease the temperature to activate the heater switch
await send_temperature_change_event(thermostat, 14, event_timestamp)
# async_track_time_interval() should have been called twice: once at startup
# while the switch was turned off, and once when the switch was turned on.
self._assert_async_mock_track_time_interval(common_mocks, 2)
# The keep-alive callback hasn't been called yet, so the only service
# call so far is to SERVICE_TURN_ON as a result of the switch turn_on()
# method being called when the target temperature increased.
self._assert_service_call(
common_mocks,
[call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"})],
)
common_mocks.mock_is_state.return_value = True
# Call the keep-alive callback a few times (as if `async_track_time_interval`
# had done it) and assert that the callback function is replaced each time.
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
# Every time the keep-alive callback is called, the home assistant switch
# turn on/off service should be called too.
self._assert_service_call(
common_mocks,
[
call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}),
call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}),
],
)
# 2. Increase the temperature to deactivate the heater switch
await send_temperature_change_event(thermostat, 20, event_timestamp)
# Simulate the end of the TPI heating cycle
await thermostat._underlyings[0].turn_off() # pylint: disable=protected-access
# turn_off() should have triggered a call to `async_track_time_interval()`
self._assert_async_mock_track_time_interval(common_mocks, 1)
# turn_off() should have triggered a call to the SERVICE_TURN_OFF service.
self._assert_service_call(
common_mocks,
[call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"})],
)
common_mocks.mock_is_state.return_value = False
# Call the keep-alive callback a few times (as if `async_track_time_interval`
# had done it) and assert that the callback function is replaced each time.
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
# Every time the keep-alive callback is called, the home assistant switch
# turn on/off service should be called too.
self._assert_service_call(
common_mocks,
[
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
],
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,6 @@
""" Test the TPI algorithm """ """ Test the TPI algorithm """
from homeassistant.components.climate import HVACMode
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -45,54 +42,53 @@ async def test_tpi_calculation(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
assert entity._prop_algorithm # pylint: disable=protected-access
tpi_algo: PropAlgorithm = entity._prop_algorithm # pylint: disable=protected-access tpi_algo = entity._prop_algorithm # pylint: disable=protected-access
assert tpi_algo assert tpi_algo
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT) tpi_algo.calculate(15, 10, 7)
assert tpi_algo.on_percent == 1 assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300 assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0 assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT) tpi_algo.calculate(15, 14, 5, False)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120 assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180 assert tpi_algo.off_time_sec == 180
tpi_algo.set_security(0.1) tpi_algo.set_security(0.1)
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT) tpi_algo.calculate(15, 14, 5, False)
assert tpi_algo.on_percent == 0.1 assert tpi_algo.on_percent == 0.1
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30) assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
assert tpi_algo.off_time_sec == 270 assert tpi_algo.off_time_sec == 270
tpi_algo.unset_security() tpi_algo.unset_security()
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT) tpi_algo.calculate(15, 14, 5, False)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120 assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180 assert tpi_algo.off_time_sec == 180
# Test minimal activation delay # Test minimal activation delay
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT) tpi_algo.calculate(15, 14.7, 15, False)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
tpi_algo.set_security(0.09) tpi_algo.set_security(0.09)
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT) tpi_algo.calculate(15, 14.7, 15, False)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
tpi_algo.unset_security() tpi_algo.unset_security()
tpi_algo.calculate(25, 30, 35, HVACMode.COOL) tpi_algo.calculate(25, 30, 35, True)
assert tpi_algo.on_percent == 1 assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300 assert tpi_algo.on_time_sec == 300
@@ -100,24 +96,9 @@ async def test_tpi_calculation(
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.set_security(0.09) tpi_algo.set_security(0.09)
tpi_algo.calculate(25, 30, 35, HVACMode.COOL) tpi_algo.calculate(25, 30, 35, True)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.unset_security()
# The calculated values for HVACMode.OFF are the same as for HVACMode.HEAT.
tpi_algo.calculate(15, 10, 7, HVACMode.OFF)
assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0
# If target_temp or current_temp are None, _calculated_on_percent is set to 0.
tpi_algo.calculate(15, None, 7, HVACMode.OFF)
assert tpi_algo.on_percent == 0
assert tpi_algo.calculated_on_percent == 0
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300

View File

@@ -37,10 +37,10 @@ async def test_over_valve_full_start(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7, PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17, PRESET_ECO + "_temp": 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19, PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21, PRESET_BOOST + "_temp": 21,
CONF_USE_WINDOW_FEATURE: True, CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True, CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
@@ -58,10 +58,10 @@ async def test_over_valve_full_start(
CONF_POWER_SENSOR: "sensor.power_sensor", CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor", CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESENCE_SENSOR: "person.presence_sensor", CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1, PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.2, PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2,
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.3, PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3,
CONF_PRESET_POWER: 10, CONF_PRESET_POWER: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
@@ -119,7 +119,7 @@ async def test_over_valve_full_start(
assert entity._prop_algorithm is not None # pylint: disable=protected-access assert entity._prop_algorithm is not None # pylint: disable=protected-access
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT # should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
# assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls( mock_send_event.assert_has_calls(
[ [
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}), call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
@@ -196,18 +196,15 @@ async def test_over_valve_full_start(
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# Change to preset Comfort # Change to preset Comfort
# Change presence to off
event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, False, True, event_timestamp)
await entity.async_set_preset_mode(preset_mode=PRESET_COMFORT) await entity.async_set_preset_mode(preset_mode=PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 17.2 # Comfort with presence off assert entity.target_temperature == 17.2
assert entity.valve_open_percent == 73 assert entity.valve_open_percent == 73
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
# Change presence to on # Change presence to on
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, True, False, event_timestamp) await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state == STATE_ON # pylint: disable=protected-access assert entity.presence_state == STATE_ON # pylint: disable=protected-access
assert entity.preset_mode is PRESET_COMFORT assert entity.preset_mode is PRESET_COMFORT
@@ -228,7 +225,7 @@ async def test_over_valve_full_start(
) as mock_service_call, patch( ) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=expected_state "homeassistant.core.StateMachine.get", return_value=expected_state
): ):
event_timestamp = now - timedelta(minutes=2) 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 True # Should be 0 but in fact 10 is send assert entity.is_device_active is True # Should be 0 but in fact 10 is send
@@ -278,7 +275,7 @@ async def test_over_valve_full_start(
assert entity.valve_open_percent == 7 assert entity.valve_open_percent == 7
# Unset the presence # Unset the presence
event_timestamp = now - timedelta(minutes=1) event_timestamp = now - timedelta(minutes=2)
await send_presence_change_event(entity, False, True, event_timestamp) await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state == STATE_OFF # pylint: disable=protected-access assert entity.presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.valve_open_percent == 10 assert entity.valve_open_percent == 10
@@ -348,10 +345,10 @@ async def test_over_valve_regulation(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7, PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17, PRESET_ECO + "_temp": 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19, PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21, PRESET_BOOST + "_temp": 21,
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,