Compare commits
37 Commits
5.3.3
...
6.0.0.alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee432dd5cd | ||
|
|
7fbdc2f0b8 | ||
|
|
744bfdb9fe | ||
|
|
d8bc2fc3d3 | ||
|
|
a396c8831f | ||
|
|
267a39b14d | ||
|
|
14a3abf402 | ||
|
|
6fa616775e | ||
|
|
d2eb581a19 | ||
|
|
15c02e9722 | ||
|
|
156d19666c | ||
|
|
ea6f2d5579 | ||
|
|
4478d65ad4 | ||
|
|
f7da58d841 | ||
|
|
8526b7d7ac | ||
|
|
12025c0610 | ||
|
|
a9595a5cf8 | ||
|
|
047c847f3c | ||
|
|
91e39f885f | ||
|
|
dce8fa2ed6 | ||
|
|
a440b35815 | ||
|
|
e52666b9d9 | ||
|
|
d9fe2bbd55 | ||
|
|
0a50d0fd4e | ||
|
|
c60f23a9ca | ||
|
|
557657a01c | ||
|
|
1f13eb4f37 | ||
|
|
4f349d6f6f | ||
|
|
76382ebb35 | ||
|
|
90f9a0e1e3 | ||
|
|
ed977b53cd | ||
|
|
5d453393f8 | ||
|
|
d2f2ab7804 | ||
|
|
b0b6d0478d | ||
|
|
f8a2c9baa9 | ||
|
|
8cbd81012c | ||
|
|
26844593b1 |
2
.devcontainer/Dockerfile
Normal file
2
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.12
|
||||
RUN apt update && apt install -y ffmpeg
|
||||
@@ -1,8 +1,5 @@
|
||||
default_config:
|
||||
|
||||
# ffmeg
|
||||
ffmpeg:
|
||||
|
||||
logger:
|
||||
default: info
|
||||
logs:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"name": "Versatile Thermostat integration",
|
||||
"appPort": [
|
||||
"8123:8123"
|
||||
@@ -9,29 +11,34 @@
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container dev-setup",
|
||||
|
||||
"mounts": [
|
||||
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||
// 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"
|
||||
],
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||
// 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"
|
||||
],
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"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",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.pylint",
|
||||
"ferrierbenjamin.fold-unfold-all-icone",
|
||||
"ms-python.isort",
|
||||
"LittleFoxTeam.vscode-python-test-adapter",
|
||||
"donjayamanne.githistory",
|
||||
"waderyan.gitblame",
|
||||
"keesschollaart.vscode-home-assistant",
|
||||
"vscode.markdown-math",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"github.vscode-github-actions"
|
||||
"github.vscode-github-actions",
|
||||
"azuretools.vscode-docker"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
@@ -52,10 +59,10 @@
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
"python.analysis.logLevel": "Trace"
|
||||
"files.trimTrailingWhitespace": true
|
||||
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
// "python.analysis.logLevel": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
.github/workflows/testus.yaml
vendored
11
.github/workflows/testus.yaml
vendored
@@ -42,9 +42,8 @@ jobs:
|
||||
|
||||
- name: Generate HTML Coverage Report
|
||||
run: coverage html
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./htmlcov
|
||||
# - name: Deploy to GitHub Pages
|
||||
# uses: peaceiris/actions-gh-pages@v3
|
||||
# with:
|
||||
# github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# publish_dir: ./htmlcov
|
||||
|
||||
30
.vscode/launch.json
vendored
30
.vscode/launch.json
vendored
@@ -1,18 +1,14 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Home Assistant (debug)",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": [
|
||||
"--debug",
|
||||
"-c",
|
||||
"config"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Home Assistant (debug)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": ["--debug", "-c", "config"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -1,21 +1,19 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modifications"
|
||||
},
|
||||
"pylint.lintOnChange": false,
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.pytestArgs": [],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.extraPaths": [
|
||||
// "/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
|
||||
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
|
||||
],
|
||||
"python.formatting.provider": "none"
|
||||
"/home/vscode/.local/lib/python3.12/site-packages/homeassistant"
|
||||
]
|
||||
}
|
||||
162
README-fr.md
162
README-fr.md
@@ -4,9 +4,9 @@
|
||||
[![hacs][hacs_badge]][hacs]
|
||||
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
|
||||
|
||||

|
||||

|
||||
|
||||
>  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. ;-).
|
||||
>  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 majeurs dans la version 5.0](#changements-majeurs-dans-la-version-50)
|
||||
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
|
||||
@@ -24,6 +24,8 @@
|
||||
- [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 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)
|
||||
- [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)
|
||||
@@ -79,19 +81,25 @@
|
||||
- [Comment être averti lorsque cela se produit ?](#comment-être-averti-lorsque-cela-se-produit-)
|
||||
- [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)
|
||||
- [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.
|
||||
|
||||
|
||||
>  _*Nouveautés*_
|
||||
>  _*Nouveautés*_
|
||||
> * **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.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
<details>
|
||||
<summary>Autres versions</summary>
|
||||
|
||||
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
|
||||
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
|
||||
@@ -110,7 +118,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
</details>
|
||||
|
||||
# Changements majeurs dans la version 5.0
|
||||

|
||||

|
||||
|
||||
Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez :
|
||||
1. Créer un VTherm de type "Configuration Centrale",
|
||||
@@ -127,7 +135,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
|
||||
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour.
|
||||
|
||||
# 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 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
@@ -150,6 +158,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,
|
||||
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
|
||||
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
|
||||
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
@@ -192,7 +201,7 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
||||
|
||||
-- VTherm = Versatile Thermostat dans la suite de ce document --
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 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.
|
||||
@@ -203,7 +212,7 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
||||
## Création d'un nouveau Versatile Thermostat
|
||||
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -211,9 +220,9 @@ Suivez ensuite les étapes de configuration comme suit :
|
||||
|
||||
## Choix des attributs de base
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Donnez les principaux attributs obligatoires :
|
||||
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
|
||||
@@ -226,14 +235,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é),
|
||||
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.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*Notes*_
|
||||
> 1. avec les types ```over_switch``` et ```over_valve```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
|
||||
> 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement.
|
||||
|
||||
## 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.
|
||||
|
||||
>  _*Comment choisir le type*_
|
||||
>  _*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 :
|
||||
> 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,
|
||||
@@ -242,18 +251,21 @@ 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```
|
||||
|
||||
### Pour un thermostat de type ```thermostat_over_switch```
|
||||

|
||||

|
||||
|
||||
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).
|
||||
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é :
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
### Pour un thermostat de type ```thermostat_over_climate```:
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -276,7 +288,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.
|
||||
|
||||
>  _*Conseil de mise en place*_
|
||||
>  _*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,
|
||||
> 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,
|
||||
@@ -363,6 +375,37 @@ 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).
|
||||
|
||||
#### 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 : 
|
||||
|
||||
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
|
||||
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.
|
||||
@@ -373,7 +416,7 @@ 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.
|
||||
|
||||
### Pour un thermostat de type ```thermostat_over_valve```:
|
||||

|
||||

|
||||
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
|
||||
|
||||
@@ -383,7 +426,7 @@ Il est possible de choisir un thermostat over valve qui commande une climatisati
|
||||
|
||||
Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` vous arriverez sur cette page :
|
||||
|
||||

|
||||

|
||||
|
||||
Vous devez donner :
|
||||
1. le coefficient coef_int de l'algorithme TPI,
|
||||
@@ -395,7 +438,7 @@ Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous ré
|
||||
## Configurer la température préréglée
|
||||
Cliquez sur 'Valider' sur la page précédente et vous y arriverez :
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
@@ -406,7 +449,7 @@ 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.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
> 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).
|
||||
@@ -421,7 +464,7 @@ La détecttion des ouvertures peut se faire de 2 manières:
|
||||
|
||||
### Le mode capteur
|
||||
En mode capteur, vous devez renseigner les informations suivantes:
|
||||

|
||||

|
||||
|
||||
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.
|
||||
@@ -429,7 +472,7 @@ En mode capteur, vous devez renseigner les informations suivantes:
|
||||
|
||||
### Le mode auto
|
||||
En mode auto, la configuration est la suivante:
|
||||

|
||||

|
||||
|
||||
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),
|
||||
@@ -441,14 +484,14 @@ Pour régler les seuils il est conseillé de commencer avec les valeurs de réf
|
||||
- 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.
|
||||

|
||||

|
||||
|
||||
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") :
|
||||

|
||||

|
||||
|
||||
Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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/)
|
||||
> 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,
|
||||
@@ -457,7 +500,7 @@ Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvert
|
||||
## 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 :
|
||||
|
||||

|
||||

|
||||
|
||||
Nous allons maintenant voir comment configurer le nouveau mode Activité.
|
||||
Ce dont nous avons besoin:
|
||||
@@ -477,21 +520,21 @@ Alors imaginons que nous voulions avoir le comportement suivant :
|
||||
|
||||
Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ».
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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
|
||||
|
||||
## 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 :
|
||||
|
||||

|
||||

|
||||
|
||||
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).
|
||||
Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
> 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.
|
||||
@@ -502,7 +545,7 @@ Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'u
|
||||
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 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,
|
||||
@@ -514,7 +557,7 @@ 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).
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*Notes*_
|
||||
> 1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
|
||||
> 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
|
||||
|
||||
@@ -522,7 +565,7 @@ ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de
|
||||
Ces paramètres permettent d'affiner le réglage du thermostat.
|
||||
Le formulaire de configuration avancée est le suivant :
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -544,7 +587,7 @@ 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
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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,
|
||||
> 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,
|
||||
@@ -564,7 +607,7 @@ 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.
|
||||
Exemple de rendu :
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
@@ -582,16 +625,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.
|
||||
|
||||
Toutes ces entités sont rattachés au service de configuration centrale :
|
||||

|
||||

|
||||
|
||||
### Configuration
|
||||
Pour configurer cette fonction, vous devez avoir une configuration centralisée (cf. [Configuration](#configuration)) et cochez la case 'Ajouter une chuadière centrale' :
|
||||
|
||||

|
||||

|
||||
|
||||
Sur la page suivante vous pouvez donner la configuration des services à appeler lors de l'allumage / extinction de la chaudière :
|
||||
|
||||

|
||||

|
||||
|
||||
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),
|
||||
@@ -614,11 +657,11 @@ Exemple:
|
||||
|
||||
Sous "Outils de développement / Service" :
|
||||
|
||||

|
||||

|
||||
|
||||
En mode yaml :
|
||||
|
||||

|
||||

|
||||
|
||||
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`)
|
||||
|
||||
@@ -663,7 +706,7 @@ context:
|
||||
|
||||
### Avertissement
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
|
||||
## Synthèse des paramètres
|
||||
@@ -687,6 +730,7 @@ context:
|
||||
| ``heater_entity2_id`` | 2ème radiateur | X | - | - | - |
|
||||
| ``heater_entity3_id`` | 3ème radiateur | X | - | - | - |
|
||||
| ``heater_entity4_id`` | 4ème radiateur | X | - | - | - |
|
||||
| ``heater_keep_alive`` | Intervalle de rafraichissement du switch | X | - | - | - |
|
||||
| ``proportional_function`` | Algorithme | X | - | - | - |
|
||||
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - | - |
|
||||
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - | - |
|
||||
@@ -735,6 +779,7 @@ context:
|
||||
| ``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 | - | - | - |
|
||||
| ``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 |
|
||||
| ``central_boiler_activation_service`` | Service d'activation de la chaudière | - | - | - | X |
|
||||
| ``central_boiler_deactivation_service`` | Service de desactivation de la chaudière | - | - | - | X |
|
||||
@@ -814,7 +859,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 :
|
||||
|
||||

|
||||

|
||||
|
||||
Dans l'ordre, il y a :
|
||||
1. l'entité principale climate de commande du thermostat,
|
||||
@@ -847,7 +892,7 @@ frontend:
|
||||
```
|
||||
et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça :
|
||||
|
||||

|
||||

|
||||
|
||||
# Services
|
||||
|
||||
@@ -893,7 +938,7 @@ target:
|
||||
entity_id: climate.my_thermostat
|
||||
```
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
|
||||
## Modifier les paramètres de sécurité
|
||||
@@ -948,7 +993,7 @@ Vous pouvez très facilement capter ses évènements dans une automatisation par
|
||||
# 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 :
|
||||

|
||||

|
||||
|
||||
Les attributs personnalisés sont les suivants :
|
||||
|
||||
@@ -999,23 +1044,23 @@ Les attributs personnalisés sont les suivants :
|
||||
# Quelques résultats
|
||||
|
||||
**Convergence de la température vers la cible configurée par preset:**
|
||||

|
||||

|
||||
|
||||
[Cycle de marche/arrêt calculé par l'intégration :](https://)
|
||||

|
||||

|
||||
|
||||
**Coef_int trop élevé (oscillations autour de la cible)**
|
||||

|
||||

|
||||
|
||||
**Évolution du calcul de l'algorithme**
|
||||

|
||||

|
||||
Voir le code de ce composant [[ci-dessous](#even-better-with-apex-chart-to-tune-your-thermostat)]
|
||||
|
||||
**Thermostat finement réglé**
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
Enjoy !
|
||||
|
||||
@@ -1054,7 +1099,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
|
||||
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 
|
||||
Vous pouvez avoir quelque chose comme ça très facilement 
|
||||
Exemple de configuration :
|
||||
|
||||
```
|
||||
@@ -1097,7 +1142,7 @@ Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour
|
||||
}
|
||||
{% endif %}
|
||||
```
|
||||

|
||||

|
||||
|
||||
## 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) :
|
||||
@@ -1170,7 +1215,7 @@ Remplacez les valeurs entre [[ ]] par les votres.
|
||||
|
||||
Exemple de courbes obtenues avec Plotly :
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
@@ -1364,11 +1409,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.
|
||||
Exemple:
|
||||
|
||||
[security mode](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome1.png?raw=true)
|
||||
[security mode](images/security-mode-symptome1.png)
|
||||
|
||||
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](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome2.png?raw=true)
|
||||
[security mode UI Card](images/security-mode-symptome2.png)
|
||||
|
||||
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**.
|
||||
|
||||
@@ -1422,6 +1467,15 @@ template: !include templates.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
***
|
||||
|
||||
[versatile_thermostat]: https://github.com/jmcollin78/versatile_thermostat
|
||||
|
||||
167
README.md
167
README.md
@@ -4,9 +4,9 @@
|
||||
[![hacs][hacs_badge]][hacs]
|
||||
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
|
||||
|
||||

|
||||

|
||||
|
||||
>  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 ;-).
|
||||
>  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 ;-).
|
||||
|
||||
- [Major changes in version 5.0](#major-changes-in-version-50)
|
||||
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
|
||||
@@ -24,6 +24,8 @@
|
||||
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
|
||||
- [Self-regulation](#self-regulation)
|
||||
- [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)
|
||||
- [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)
|
||||
@@ -41,7 +43,7 @@
|
||||
- [How to find the right service?](#how-to-find-the-right-service)
|
||||
- [The events](#the-events)
|
||||
- [Warning](#warning)
|
||||
- [Parameters synthesis](#parameters-synthesis)
|
||||
- [Parameter summary](#parameter-summary)
|
||||
- [Examples tuning](#examples-tuning)
|
||||
- [Electrical heater](#electrical-heater)
|
||||
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
|
||||
@@ -79,19 +81,25 @@
|
||||
- [How can I be notified when this happens?](#how-can-i-be-notified-when-this-happens)
|
||||
- [How to repair?](#how-to-repair)
|
||||
- [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.
|
||||
|
||||
> _*News*_
|
||||
> _*News*_
|
||||
> * **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.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
|
||||
> * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
<details>
|
||||
<summary>Others releases</summary>
|
||||
|
||||
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
|
||||
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
|
||||
@@ -110,7 +118,7 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
|
||||
</details>
|
||||
|
||||
# Major changes in 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:
|
||||
1. Create a VTherm of type “Central Configuration”,
|
||||
@@ -127,7 +135,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
|
||||
**Note:** the VTherm configuration screenshots have not been updated.
|
||||
|
||||
# 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 for the beers. It's very nice and encourages me to continue!
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou for the beers. It's very nice and encourages me to continue!
|
||||
|
||||
# When to use / not use
|
||||
This thermostat can control 3 types of equipment:
|
||||
@@ -150,6 +158,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.
|
||||
3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
|
||||
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
|
||||
5. TRV of type Aqara SRTS-A01 and MOES TV01-ZB which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
@@ -192,7 +201,7 @@ This component named __Versatile thermostat__ manage the following use cases :
|
||||
|
||||
-- VTherm = Versatile Thermostat in the remainder of this document --
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 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.
|
||||
@@ -202,7 +211,7 @@ This component named __Versatile thermostat__ manage the following use cases :
|
||||
|
||||
## Creation of a new Versatile Thermostat
|
||||
Click on Add integration button in the integration page
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -210,9 +219,9 @@ Then follow the configurations steps as follow:
|
||||
|
||||
## Minimal configuration update
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Give the main mandatory attributes:
|
||||
1. a name (will be the name of the integration and also the name of the climate entity)
|
||||
@@ -225,14 +234,14 @@ Give the main mandatory attributes:
|
||||
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.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*Notes*_
|
||||
> 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
|
||||
> 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
|
||||
|
||||
## 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.
|
||||
|
||||
>  _*How to choose the type*_
|
||||
>  _*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:
|
||||
> 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,
|
||||
@@ -242,18 +251,21 @@ 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.
|
||||
|
||||
### For a ```thermostat_over_switch``` type thermostat
|
||||

|
||||

|
||||
|
||||
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).
|
||||
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:
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
### For a thermostat of type ```thermostat_over_climate```:
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -276,7 +288,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.
|
||||
|
||||
>  _*Implementation tip*_
|
||||
>  _*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,
|
||||
> 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,
|
||||
@@ -361,6 +373,36 @@ 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).
|
||||
|
||||
#### 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: 
|
||||
|
||||
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
|
||||
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.
|
||||
@@ -370,7 +412,7 @@ 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.
|
||||
|
||||
### For a thermostat of type ```thermostat_over_valve```:
|
||||

|
||||

|
||||
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).
|
||||
|
||||
@@ -378,13 +420,13 @@ It is possible to choose an over valve thermostat which controls air conditionin
|
||||
|
||||
## 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:
|
||||

|
||||

|
||||
|
||||
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
|
||||
|
||||
## Configure the preset temperature
|
||||
Click on 'Validate' on the previous page and you will get there:
|
||||

|
||||

|
||||
|
||||
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
|
||||
@@ -395,7 +437,7 @@ 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.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
> 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).
|
||||
@@ -410,14 +452,14 @@ The detection of openings can be done in 2 ways:
|
||||
|
||||
### The sensor mode
|
||||
In sensor mode, you must fill in the following information:
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
### Auto mode
|
||||
In auto mode, the configuration is as follows:
|
||||

|
||||

|
||||
|
||||
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),
|
||||
@@ -429,14 +471,14 @@ To set the thresholds it is advisable to start with the reference values a
|
||||
- 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.
|
||||

|
||||

|
||||
|
||||
To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"):
|
||||

|
||||

|
||||
|
||||
And that's all ! your thermostat will turn off when the windows are open and turn back on when closed.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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/)
|
||||
> 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,
|
||||
@@ -444,7 +486,7 @@ And that's all ! your thermostat will turn off when the windows are open and tur
|
||||
|
||||
## 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:
|
||||

|
||||

|
||||
|
||||
We will now see how to configure the new Activity mode.
|
||||
What we need:
|
||||
@@ -463,13 +505,13 @@ What we need:
|
||||
|
||||
For this to work, the climate thermostat should be in ``Activity`` preset mode.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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
|
||||
|
||||
## Configure the power management
|
||||
|
||||
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -477,7 +519,7 @@ 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).
|
||||
This allows you to change the max power along time using a Scheduler or whatever you like.
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
> 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.
|
||||
@@ -488,7 +530,7 @@ This allows you to change the max power along time using a Scheduler or whatever
|
||||
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 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,
|
||||
@@ -500,7 +542,7 @@ 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).
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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,
|
||||
> 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.
|
||||
|
||||
@@ -508,7 +550,7 @@ ATTENTION: groups of people do not function as a presence sensor. They are not r
|
||||
Those parameters allows to fine tune the thermostat.
|
||||
The advanced configuration form is the following:
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -530,7 +572,7 @@ 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
|
||||
|
||||
> _*Notes*_
|
||||
> _*Notes*_
|
||||
> 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,
|
||||
> 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.
|
||||
@@ -550,7 +592,7 @@ 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.
|
||||
Example rendering:
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
@@ -568,16 +610,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.
|
||||
|
||||
All these entities are attached to the central configuration service:
|
||||

|
||||

|
||||
|
||||
### Setup
|
||||
To configure this function, you must have a centralized configuration (see [Configuration](#configuration)) and check the 'Add a central boiler' box:
|
||||
|
||||

|
||||

|
||||
|
||||
On the following page you can configure the services to be called when switching the boiler on/off:
|
||||
|
||||

|
||||

|
||||
|
||||
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),
|
||||
@@ -600,11 +642,11 @@ Example:
|
||||
|
||||
Under "Development Tools / Service":
|
||||
|
||||

|
||||

|
||||
|
||||
In yaml mode:
|
||||
|
||||

|
||||

|
||||
|
||||
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`)
|
||||
|
||||
@@ -649,12 +691,12 @@ context:
|
||||
|
||||
### Warning
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
|
||||
## Parameters synthesis
|
||||
## Parameter summary
|
||||
|
||||
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "central configuration" |
|
||||
| Parameter | Description | "over switch" | "over climate" | "over valve" | "central configuration" |
|
||||
| ----------------------------------------- | ----------------------------------------------------------------------------- | ------------- | ------------------- | ------------ | ----------------------- |
|
||||
| ``name`` | Name | X | X | X | - |
|
||||
| ``thermostat_type`` | Thermostat type | X | X | X | - |
|
||||
@@ -673,6 +715,7 @@ context:
|
||||
| ``heater_entity2_id`` | 2nd heater switch | X | - | - | - |
|
||||
| ``heater_entity3_id`` | 3rd heater switch | X | - | - | - |
|
||||
| ``heater_entity4_id`` | 4th heater switch | X | - | - | - |
|
||||
| ``heater_keep_alive`` | Switch keep-alive interval | X | - | - | - |
|
||||
| ``proportional_function`` | Algorithm | X | - | X | - |
|
||||
| ``climate_entity1_id`` | 1rst underlying climate | - | X | - | - |
|
||||
| ``climate_entity2_id`` | 2nd underlying climate | - | X | - | - |
|
||||
@@ -722,6 +765,7 @@ context:
|
||||
| ``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 | - | - | - |
|
||||
| ``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 |
|
||||
| ``central_boiler_activation_service`` | Activation service of the boiler | - | - | - | X |
|
||||
| ``central_boiler_deactivation_service`` | Deactivaiton service of the boiler | - | - | - | X |
|
||||
@@ -802,7 +846,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:
|
||||
|
||||

|
||||

|
||||
|
||||
In order, there are:
|
||||
1. the main climate thermostat command entity,
|
||||
@@ -835,7 +879,7 @@ frontend:
|
||||
```
|
||||
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
|
||||
|
||||

|
||||

|
||||
|
||||
# Services
|
||||
|
||||
@@ -881,7 +925,7 @@ target:
|
||||
entity_id: climate.my_thermostat
|
||||
```
|
||||
|
||||
>  _*Notes*_
|
||||
>  _*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.
|
||||
|
||||
## Change safety settings
|
||||
@@ -934,7 +978,7 @@ You can very easily capture its events in an automation, for example to notify u
|
||||
# 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:
|
||||

|
||||

|
||||
|
||||
Custom attributes are the following:
|
||||
|
||||
@@ -985,23 +1029,23 @@ Custom attributes are the following:
|
||||
# Some results
|
||||
|
||||
**Convergence of temperature to target configured by preset:**
|
||||

|
||||

|
||||
|
||||
[Cycle of on/off calculated by the integration:](https://)
|
||||

|
||||

|
||||
|
||||
**Coef_int too high (oscillations around the target)**
|
||||

|
||||

|
||||
|
||||
**Algorithm calculation evolution**
|
||||

|
||||

|
||||
See the code of this component [[below](#even-better-with-apex-chart-to-tune-your-thermostat)]
|
||||
|
||||
**Fine tuned thermostat**
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
Enjoy !
|
||||
|
||||
@@ -1040,7 +1084,7 @@ I hope this example helps you, don't hesitate to give me your feedbacks !
|
||||
|
||||
## 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.
|
||||
You can have something like that very easily 
|
||||
You can have something like that very easily 
|
||||
Example configuration:
|
||||
|
||||
```
|
||||
@@ -1082,7 +1126,7 @@ You can customize this component using the HACS card-mod component to adjust the
|
||||
}
|
||||
{% endif %}
|
||||
```
|
||||

|
||||

|
||||
|
||||
## 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):
|
||||
@@ -1155,7 +1199,7 @@ Replace values in [[ ]] by yours.
|
||||
|
||||
Example of graph obtained with Plotly :
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## And always better and better with the NOTIFIER daemon app to notify events
|
||||
@@ -1348,11 +1392,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.
|
||||
Example:
|
||||
|
||||
[safety mode](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome1.png?raw=true)
|
||||
[safety mode](images/security-mode-symptome1.png)
|
||||
|
||||
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](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome2.png?raw=true)
|
||||
[safety mode UI Card](images/security-mode-symptome2.png)
|
||||
|
||||
You can also check in the VTherm attributes the dates of receipt of the different dates. Attributes are available in Development Tools / Reports.
|
||||
|
||||
@@ -1406,6 +1450,15 @@ template: !include templates.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
***
|
||||
|
||||
[versatile_thermostat]: https://github.com/jmcollin78/versatile_thermostat
|
||||
|
||||
7
copy-to-forum.txt
Normal file
7
copy-to-forum.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Before copying to forum you need to replace relative images by this command into VSCode:
|
||||
|
||||
Search :
|
||||
\(images/(.*).png\)
|
||||
|
||||
Replace with:
|
||||
(https://github.com/jmcollin78/versatile_thermostat/blob/main/images/$1.png?raw=true)
|
||||
@@ -8,10 +8,10 @@ import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigType
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
@@ -82,15 +82,27 @@ async def async_setup(
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
# L'argument config contient votre fichier configuration.yaml
|
||||
vtherm_config = config.get(DOMAIN)
|
||||
|
||||
if vtherm_config is not None:
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
api.set_global_config(vtherm_config)
|
||||
else:
|
||||
_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(
|
||||
DOMAIN,
|
||||
SERVICE_RELOAD,
|
||||
@@ -114,6 +126,7 @@ async def reload_all_vtherm(hass):
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -134,6 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
|
||||
return True
|
||||
|
||||
@@ -148,6 +162,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api is not None:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -6,6 +6,8 @@ import math
|
||||
import logging
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.core import (
|
||||
@@ -20,10 +22,12 @@ from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_call_later,
|
||||
EventStateChangedData,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
@@ -78,9 +82,9 @@ from .const import (
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PRESETS,
|
||||
CONF_PRESETS_AWAY,
|
||||
CONF_PRESETS_WITH_AC,
|
||||
CONF_PRESETS_AWAY_WITH_AC,
|
||||
# CONF_PRESETS_AWAY,
|
||||
# CONF_PRESETS_WITH_AC,
|
||||
# CONF_PRESETS_AWAY_WITH_AC,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_TPI_COEF_INT,
|
||||
@@ -107,6 +111,7 @@ from .const import (
|
||||
CONF_USE_POWER_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_TEMP_MAX,
|
||||
CONF_TEMP_MIN,
|
||||
HIDDEN_PRESETS,
|
||||
@@ -134,6 +139,7 @@ from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from .ema import ExponentialMovingAverage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
@@ -145,20 +151,6 @@ def get_tz(hass: HomeAssistant):
|
||||
class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Representation of a base class for all Versatile Thermostat device."""
|
||||
|
||||
# The list of VersatileThermostat entities
|
||||
_hass: HomeAssistant
|
||||
_last_temperature_measure: datetime
|
||||
_last_ext_temperature_measure: datetime
|
||||
_total_energy: float
|
||||
_overpowering_state: bool
|
||||
_window_state: bool
|
||||
_motion_state: bool
|
||||
_presence_state: bool
|
||||
_window_auto_state: bool
|
||||
_window_bypass_state: bool
|
||||
_underlyings: list[UnderlyingEntity]
|
||||
_last_change_time: datetime
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
ClimateEntity._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
@@ -211,12 +203,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id: str,
|
||||
name: str,
|
||||
entry_infos: ConfigData,
|
||||
):
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._hass = hass
|
||||
self._entry_infos = None
|
||||
self._attr_extra_state_attributes = {}
|
||||
|
||||
self._unique_id = unique_id
|
||||
@@ -276,7 +275,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._last_change_time = None
|
||||
|
||||
self._underlyings = []
|
||||
self._underlyings: list[UnderlyingEntity] = []
|
||||
|
||||
self._ema_temp = None
|
||||
self._ema_algo = None
|
||||
@@ -288,9 +287,20 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._last_central_mode = None
|
||||
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)
|
||||
|
||||
def clean_central_config_doublon(self, config_entry, central_config) -> dict:
|
||||
def clean_central_config_doublon(
|
||||
self, config_entry: ConfigData, central_config: ConfigEntry | None
|
||||
) -> dict[str, Any]:
|
||||
"""Removes all values from config with are concerned by central_config"""
|
||||
|
||||
def clean_one(cfg, schema: vol.Schema):
|
||||
@@ -308,10 +318,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if cfg.get(CONF_USE_TPI_CENTRAL_CONFIG) is True:
|
||||
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:
|
||||
clean_one(cfg, STEP_CENTRAL_WINDOW_DATA_SCHEMA)
|
||||
|
||||
@@ -336,7 +342,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
return entry_infos
|
||||
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Finish the initialization of the thermostast"""
|
||||
|
||||
_LOGGER.info(
|
||||
@@ -352,38 +358,22 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
_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._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
|
||||
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
|
||||
if (step := entry_infos.get(CONF_STEP_TEMPERATURE)) is not None:
|
||||
self._attr_target_temperature_step = step
|
||||
|
||||
# convert entry_infos into usable attributes
|
||||
presets = {}
|
||||
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 = {}
|
||||
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
|
||||
)
|
||||
self._attr_preset_modes: list[str] | None
|
||||
|
||||
if self._window_call_cancel is not None:
|
||||
self._window_call_cancel()
|
||||
@@ -400,8 +390,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._proportional_function = entry_infos.get(CONF_PROP_FUNCTION)
|
||||
self._temp_sensor_entity_id = entry_infos.get(CONF_TEMP_SENSOR)
|
||||
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
|
||||
# Default value not configurable
|
||||
self._attr_target_temperature_step = 0.1
|
||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
|
||||
@@ -447,7 +435,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
|
||||
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||
|
||||
self._presence_on = self._presence_sensor_entity_id is not None
|
||||
self._presence_on = (
|
||||
entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
|
||||
and self._presence_sensor_entity_id is not None
|
||||
)
|
||||
|
||||
if self._ac_mode:
|
||||
# Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
|
||||
@@ -463,15 +454,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._support_flags = SUPPORT_FLAGS
|
||||
|
||||
self._presets = presets
|
||||
self._presets_away = presets_away
|
||||
# Preset will be initialized from Number entities
|
||||
self._presets: dict[str, Any] = {} # presets
|
||||
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
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
self._saved_preset_mode = PRESET_NONE
|
||||
@@ -535,24 +521,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._overpowering_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
|
||||
|
||||
# Read the parameter from configuration.yaml if it exists
|
||||
@@ -800,11 +768,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
|
||||
self.reset_last_change_time()
|
||||
|
||||
await self.get_my_previous_state()
|
||||
@@ -819,7 +785,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
def init_underlyings(self):
|
||||
"""Initialize all underlyings. Should be overriden if necessary"""
|
||||
|
||||
def restore_specific_previous_state(self, old_state):
|
||||
def restore_specific_previous_state(self, old_state: State):
|
||||
"""Should be overriden in each specific thermostat
|
||||
if a specific previous state or attribute should be
|
||||
restored
|
||||
@@ -853,16 +819,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
old_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
|
||||
# Never restore a Power or Security preset
|
||||
if (
|
||||
old_preset_mode in self._attr_preset_modes
|
||||
and old_preset_mode not in HIDDEN_PRESETS
|
||||
):
|
||||
if old_preset_mode is not None and old_preset_mode not in HIDDEN_PRESETS:
|
||||
# old_preset_mode in self._attr_preset_modes
|
||||
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
|
||||
self.save_preset_mode()
|
||||
else:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
|
||||
if not self._hvac_mode and old_state.state:
|
||||
if not self._hvac_mode and old_state.state in [
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.COOL,
|
||||
]:
|
||||
self._hvac_mode = old_state.state
|
||||
else:
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
@@ -900,7 +868,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._hvac_mode,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@property
|
||||
@@ -930,19 +898,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
def should_poll(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""List of available operation modes."""
|
||||
return self._hvac_list
|
||||
|
||||
@@ -1030,17 +998,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
return self._is_used_by_central_boiler
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
def is_device_active(self) -> bool:
|
||||
"""Returns true if one underlying is active"""
|
||||
for under in self._underlyings:
|
||||
if under.is_device_active:
|
||||
@@ -1048,7 +1016,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@@ -1187,6 +1155,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
Is None if the VTherm is not controlled by 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:
|
||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||
if index < self.nb_underlying_entities:
|
||||
@@ -1205,19 +1178,23 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Turn auxiliary heater on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@overrides
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@overrides
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@overrides
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True):
|
||||
@overrides
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode, need_control_heating=True):
|
||||
"""Set new target hvac mode."""
|
||||
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
|
||||
|
||||
@@ -1234,9 +1211,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
|
||||
if self._hvac_mode == HVACMode.COOL:
|
||||
if self._hvac_mode == HVACMode.COOL and self.preset_mode != PRESET_NONE:
|
||||
if self.preset_mode != PRESET_FROST_PROTECTION:
|
||||
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
|
||||
await self._async_set_preset_mode_internal(self.preset_mode, True)
|
||||
else:
|
||||
await self._async_set_preset_mode_internal(PRESET_ECO, True, False)
|
||||
|
||||
@@ -1253,7 +1230,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
|
||||
@overrides
|
||||
async def async_set_preset_mode(self, preset_mode, overwrite_saved_preset=True):
|
||||
async def async_set_preset_mode(
|
||||
self, preset_mode: str, overwrite_saved_preset=True
|
||||
):
|
||||
"""Set new preset mode."""
|
||||
await self._async_set_preset_mode_internal(
|
||||
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
|
||||
@@ -1261,7 +1240,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_set_preset_mode_internal(
|
||||
self, preset_mode, force=False, overwrite_saved_preset=True
|
||||
self, preset_mode: str, force=False, overwrite_saved_preset=True
|
||||
):
|
||||
"""Set new preset mode."""
|
||||
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
|
||||
@@ -1273,7 +1252,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long
|
||||
)
|
||||
|
||||
if preset_mode == self._attr_preset_mode and not force:
|
||||
old_preset_mode = self._attr_preset_mode
|
||||
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
|
||||
return
|
||||
|
||||
@@ -1306,27 +1286,30 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
if overwrite_saved_preset:
|
||||
self.save_preset_mode()
|
||||
|
||||
self.recalculate()
|
||||
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
||||
# Notify only if there was a real change
|
||||
if self._attr_preset_mode != old_preset_mode:
|
||||
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
||||
|
||||
def reset_last_change_time(
|
||||
self, old_preset_mode=None
|
||||
self, old_preset_mode: str | None = None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Reset to now the last change time"""
|
||||
self._last_change_time = datetime.now(tz=self._current_tz)
|
||||
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
|
||||
|
||||
def reset_last_temperature_time(self, old_preset_mode=None):
|
||||
def reset_last_temperature_time(self, old_preset_mode: str | None = None):
|
||||
"""Reset to now the last temperature time if conditions are satisfied"""
|
||||
if (
|
||||
self._attr_preset_mode not in HIDDEN_PRESETS
|
||||
and old_preset_mode not in HIDDEN_PRESETS
|
||||
):
|
||||
self._last_temperature_measure = (
|
||||
self._last_ext_temperature_measure
|
||||
) = datetime.now(tz=self._current_tz)
|
||||
self._last_temperature_measure = self._last_ext_temperature_measure = (
|
||||
datetime.now(tz=self._current_tz)
|
||||
)
|
||||
|
||||
def find_preset_temp(self, preset_mode):
|
||||
def find_preset_temp(self, preset_mode: str):
|
||||
"""Find the right temperature of a preset considering the presence if configured"""
|
||||
if preset_mode is None or preset_mode == "none":
|
||||
return (
|
||||
@@ -1343,9 +1326,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
return self._power_temp
|
||||
if preset_mode == PRESET_ACTIVITY:
|
||||
return self._presets[
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
(
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
]
|
||||
else:
|
||||
# Select _ac presets if in COOL Mode (or over_switch with _ac_mode)
|
||||
@@ -1354,19 +1339,28 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
_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 [
|
||||
None,
|
||||
STATE_ON,
|
||||
STATE_HOME,
|
||||
]:
|
||||
return self._presets[preset_mode]
|
||||
return temp_val
|
||||
else:
|
||||
return self._presets_away[self.get_preset_away_name(preset_mode)]
|
||||
# We should return the preset_away temp val but if
|
||||
# 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):
|
||||
def get_preset_away_name(self, preset_mode: str) -> str:
|
||||
"""Get the preset name in away mode (when presence is off)"""
|
||||
return preset_mode + PRESET_AWAY_SUFFIX
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode: str):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
return
|
||||
@@ -1376,7 +1370,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
return
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
async def async_set_swing_mode(self, swing_mode: str):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
return
|
||||
@@ -1393,14 +1387,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.reset_last_change_time()
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
"""
|
||||
self._target_temp = temperature
|
||||
return
|
||||
|
||||
def get_state_date_or_now(self, state: State):
|
||||
def get_state_date_or_now(self, state: State) -> datetime:
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_changed.astimezone(self._current_tz)
|
||||
@@ -1408,7 +1402,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
else datetime.now(tz=self._current_tz)
|
||||
)
|
||||
|
||||
def get_last_updated_date_or_now(self, state: State):
|
||||
def get_last_updated_date_or_now(self, state: State) -> datetime:
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_updated.astimezone(self._current_tz)
|
||||
@@ -1693,7 +1687,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_power_changed(self, event):
|
||||
async def _async_power_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle power changes."""
|
||||
_LOGGER.debug("Thermostat %s - Receive new Power event", self.name)
|
||||
_LOGGER.debug(event)
|
||||
@@ -1719,7 +1713,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_max_power_changed(self, event):
|
||||
async def _async_max_power_changed(
|
||||
self, event: HASSEventType[EventStateChangedData]
|
||||
):
|
||||
"""Handle power max changes."""
|
||||
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
|
||||
_LOGGER.debug(event)
|
||||
@@ -1744,7 +1740,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_presence_changed(self, event):
|
||||
async def _async_presence_changed(
|
||||
self, event: HASSEventType[EventStateChangedData]
|
||||
):
|
||||
"""Handle presence changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
@@ -1760,7 +1758,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self._async_update_presence(new_state.state)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_update_presence(self, new_state):
|
||||
async def _async_update_presence(self, new_state: str):
|
||||
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
|
||||
self._presence_state = (
|
||||
STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
|
||||
@@ -1807,9 +1805,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
await self._async_internal_set_temperature(
|
||||
self._presets[
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
(
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
]
|
||||
)
|
||||
_LOGGER.debug(
|
||||
@@ -1873,7 +1873,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
if self.window_bypass_state or not self.is_window_auto_enabled:
|
||||
_LOGGER.info(
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
|
||||
self,
|
||||
)
|
||||
@@ -2058,7 +2058,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
return self._overpowering_state
|
||||
|
||||
async def check_central_mode(self, new_central_mode, old_central_mode) -> None:
|
||||
async def check_central_mode(
|
||||
self, new_central_mode: str | None, old_central_mode: str | None
|
||||
):
|
||||
"""Take into account a central mode change"""
|
||||
if not self.is_controlled_by_central_mode:
|
||||
self._last_central_mode = None
|
||||
@@ -2348,7 +2350,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
else: # default is to turn_off
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -2416,7 +2418,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
def update_custom_attributes(self):
|
||||
"""Update the custom extra attributes for the entity"""
|
||||
|
||||
self._attr_extra_state_attributes: dict(str, str) = {
|
||||
self._attr_extra_state_attributes: dict[str, Any] = {
|
||||
"is_on": self.is_on,
|
||||
"hvac_action": self.hvac_action,
|
||||
"hvac_mode": self.hvac_mode,
|
||||
@@ -2424,21 +2426,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"type": self._thermostat_type,
|
||||
"is_controlled_by_central_mode": self.is_controlled_by_central_mode,
|
||||
"last_central_mode": self.last_central_mode,
|
||||
"frost_temp": self._presets[PRESET_FROST_PROTECTION],
|
||||
"eco_temp": self._presets[PRESET_ECO],
|
||||
"boost_temp": self._presets[PRESET_BOOST],
|
||||
"comfort_temp": self._presets[PRESET_COMFORT],
|
||||
"frost_temp": self._presets.get(PRESET_FROST_PROTECTION, 0),
|
||||
"eco_temp": self._presets.get(PRESET_ECO, 0),
|
||||
"boost_temp": self._presets.get(PRESET_BOOST, 0),
|
||||
"comfort_temp": self._presets.get(PRESET_COMFORT, 0),
|
||||
"frost_away_temp": self._presets_away.get(
|
||||
self.get_preset_away_name(PRESET_FROST_PROTECTION)
|
||||
self.get_preset_away_name(PRESET_FROST_PROTECTION), 0
|
||||
),
|
||||
"eco_away_temp": self._presets_away.get(
|
||||
self.get_preset_away_name(PRESET_ECO)
|
||||
self.get_preset_away_name(PRESET_ECO), 0
|
||||
),
|
||||
"boost_away_temp": self._presets_away.get(
|
||||
self.get_preset_away_name(PRESET_BOOST)
|
||||
self.get_preset_away_name(PRESET_BOOST), 0
|
||||
),
|
||||
"comfort_away_temp": self._presets_away.get(
|
||||
self.get_preset_away_name(PRESET_COMFORT)
|
||||
self.get_preset_away_name(PRESET_COMFORT), 0
|
||||
),
|
||||
"power_temp": self._power_temp,
|
||||
"target_temperature_step": self.target_temperature_step,
|
||||
@@ -2497,7 +2499,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""
|
||||
_LOGGER.info("%s - The config entry have been updated")
|
||||
|
||||
async def service_set_presence(self, presence):
|
||||
async def service_set_presence(self, presence: str):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_presence
|
||||
data:
|
||||
@@ -2510,7 +2512,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def service_set_preset_temperature(
|
||||
self, preset, temperature=None, temperature_away=None
|
||||
self,
|
||||
preset: str,
|
||||
temperature: float | None = None,
|
||||
temperature_away: float | None = None,
|
||||
):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_preset_temperature
|
||||
@@ -2548,7 +2553,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def service_set_security(self, delay_min, min_on_percent, default_on_percent):
|
||||
async def service_set_security(
|
||||
self,
|
||||
delay_min: int | None,
|
||||
min_on_percent: float | None,
|
||||
default_on_percent: float | None,
|
||||
):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_security
|
||||
data:
|
||||
@@ -2578,7 +2588,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating()
|
||||
self.update_custom_attributes()
|
||||
|
||||
async def service_set_window_bypass_state(self, window_bypass):
|
||||
async def service_set_window_bypass_state(self, window_bypass: bool):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_window_bypass
|
||||
data:
|
||||
@@ -2611,8 +2621,78 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
def send_event(self, event_type: EventType, data: dict):
|
||||
"""Send an event"""
|
||||
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)
|
||||
# data["entity_id"] = self.entity_id
|
||||
# data["name"] = self.name
|
||||
# data["state_attributes"] = self.state_attributes
|
||||
# self._hass.bus.fire(event_type.value, data)
|
||||
|
||||
async def init_presets(self, central_config):
|
||||
"""Init all presets of the VTherm"""
|
||||
# If preset central config is used and central config is set , take the presets from central config
|
||||
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
|
||||
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())
|
||||
|
||||
@@ -7,11 +7,11 @@ from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
CoreState,
|
||||
# CoreState,
|
||||
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.event import async_track_state_change_event
|
||||
@@ -386,17 +386,18 @@ class CentralBoilerBinarySensor(BinarySensorEntity):
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
api.register_central_boiler(self)
|
||||
|
||||
@callback
|
||||
async def _async_startup_internal(*_):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
await self.listen_nb_active_vtherm_entity()
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
await _async_startup_internal()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
)
|
||||
# Should be not more needed and replaced by vtherm_api.init_vtherm_links
|
||||
# @callback
|
||||
# async def _async_startup_internal(*_):
|
||||
# _LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
# await self.listen_nb_active_vtherm_entity()
|
||||
#
|
||||
# if self.hass.state == CoreState.running:
|
||||
# await _async_startup_internal()
|
||||
# else:
|
||||
# self.hass.bus.async_listen_once(
|
||||
# EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
# )
|
||||
|
||||
async def listen_nb_active_vtherm_entity(self):
|
||||
"""Initialize the listening of state change of VTherms"""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" Implements the VersatileThermostat climate component """
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
@@ -44,9 +45,6 @@ from .thermostat_valve import ThermostatOverValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# _LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" Some usefull commons class """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
@@ -182,6 +183,9 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
"""Returns my climate if found"""
|
||||
if not self._my_climate:
|
||||
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
|
||||
|
||||
@property
|
||||
@@ -238,6 +242,11 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
|
||||
await try_find_climate(None)
|
||||
|
||||
@callback
|
||||
def my_climate_is_initialized(self):
|
||||
"""Called when the associated climate is initialized"""
|
||||
return
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(
|
||||
self, event: Event
|
||||
|
||||
@@ -95,9 +95,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
self._init_feature_flags(infos)
|
||||
self._init_central_config_flags(infos)
|
||||
|
||||
def _init_feature_flags(self, infos):
|
||||
def _init_feature_flags(self, _):
|
||||
"""Fix features selection depending to infos"""
|
||||
is_empty: bool = not bool(infos)
|
||||
is_empty: bool = False # TODO remove this not bool(infos)
|
||||
self._infos[CONF_USE_WINDOW_FEATURE] = (
|
||||
is_empty
|
||||
or self._infos.get(CONF_WINDOW_SENSOR) is not None
|
||||
@@ -128,7 +128,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
):
|
||||
if not is_empty:
|
||||
self._infos[config] = self._infos.get(config) is True
|
||||
current_config = self._infos.get(config, None)
|
||||
self._infos[config] = current_config is True or (
|
||||
current_config is None and self._central_config is not None
|
||||
)
|
||||
else:
|
||||
self._infos[config] = self._central_config is not None
|
||||
|
||||
@@ -203,6 +206,70 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
except ServiceConfigurationError as 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)"""
|
||||
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):
|
||||
"""For each schema entry not in user_input, set or remove values in infos"""
|
||||
self._infos.update(user_input)
|
||||
@@ -244,6 +311,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
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)
|
||||
return await next_step_function()
|
||||
|
||||
@@ -264,30 +333,72 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_main
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_menu
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
menu_options = ["main", "type"]
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
menu_options.append("central_boiler")
|
||||
else:
|
||||
menu_options.append("features")
|
||||
|
||||
if self._infos.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
|
||||
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 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")
|
||||
|
||||
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:
|
||||
"""Handle the flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_main user_input=%s", user_input)
|
||||
|
||||
schema = STEP_MAIN_DATA_SCHEMA
|
||||
next_step = self.async_step_type
|
||||
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME
|
||||
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
|
||||
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
|
||||
else:
|
||||
schema = STEP_MAIN_DATA_SCHEMA
|
||||
# If we come from async_step_spec_main
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_main":
|
||||
next_step = self.async_step_type
|
||||
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
|
||||
|
||||
if (
|
||||
user_input
|
||||
and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False
|
||||
):
|
||||
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)
|
||||
|
||||
@@ -299,7 +410,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
|
||||
else:
|
||||
schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA
|
||||
next_step = self.async_step_type
|
||||
next_step = self.async_step_menu
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_main"
|
||||
|
||||
@@ -315,7 +426,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
)
|
||||
|
||||
schema = STEP_CENTRAL_BOILER_SCHEMA
|
||||
next_step = self.async_step_tpi
|
||||
next_step = self.async_step_menu
|
||||
|
||||
return await self.generic_step("central_boiler", schema, user_input, next_step)
|
||||
|
||||
@@ -325,36 +436,50 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
|
||||
return await self.generic_step(
|
||||
"type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
|
||||
"type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_menu
|
||||
)
|
||||
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
|
||||
return await self.generic_step(
|
||||
"type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
|
||||
"type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_menu
|
||||
)
|
||||
else:
|
||||
return await self.generic_step(
|
||||
"type",
|
||||
STEP_THERMOSTAT_CLIMATE,
|
||||
user_input,
|
||||
self.async_step_presets,
|
||||
self.async_step_menu,
|
||||
)
|
||||
|
||||
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_FEATURES_DATA_SCHEMA,
|
||||
user_input,
|
||||
self.async_step_menu,
|
||||
)
|
||||
|
||||
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the TPI flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
|
||||
next_step = self.async_step_presets
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_tpi":
|
||||
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
|
||||
else:
|
||||
schema = STEP_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)
|
||||
|
||||
@@ -364,7 +489,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
|
||||
self._infos[COMES_FROM] = "async_step_spec_tpi"
|
||||
next_step = self.async_step_presets
|
||||
next_step = self.async_step_menu
|
||||
|
||||
return await self.generic_step("tpi", schema, user_input, next_step)
|
||||
|
||||
@@ -372,82 +497,41 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the presets flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input)
|
||||
|
||||
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
|
||||
next_step = self.async_step_menu # advanced
|
||||
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 presets_with_ac and goto windows
|
||||
# In Central config -> display the next step immedialty
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA
|
||||
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
|
||||
# Call directly the next step, we have nothing to display here
|
||||
return await self.async_step_window() # = self.async_step_window
|
||||
|
||||
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:
|
||||
"""Handle the window sensor flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input)
|
||||
|
||||
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
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
|
||||
next_step = self.async_step_motion
|
||||
# If comes from async_step_spec_window
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_window":
|
||||
# If we have a window sensor don't display the auto window parameters
|
||||
if self._infos.get(CONF_WINDOW_SENSOR) is not None:
|
||||
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
|
||||
else:
|
||||
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
|
||||
elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_window
|
||||
else:
|
||||
schema = STEP_WINDOW_DATA_SCHEMA
|
||||
|
||||
if (
|
||||
user_input
|
||||
and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG, False) is False
|
||||
):
|
||||
if (
|
||||
user_input
|
||||
and self._infos.get(COMES_FROM) == "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)
|
||||
|
||||
@@ -474,23 +558,24 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the window and motion sensor flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input)
|
||||
|
||||
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
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
|
||||
next_step = self.async_step_power
|
||||
# If comes from async_step_spec_motion
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_motion":
|
||||
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
|
||||
elif user_input and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_motion
|
||||
else:
|
||||
schema = STEP_MOTION_DATA_SCHEMA
|
||||
|
||||
if (
|
||||
user_input
|
||||
and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG, False) is False
|
||||
):
|
||||
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)
|
||||
|
||||
@@ -506,7 +591,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_motion"
|
||||
|
||||
next_step = self.async_step_power
|
||||
next_step = self.async_step_menu
|
||||
|
||||
# This will return to async_step_main (to keep the "main" step)
|
||||
return await self.generic_step("motion", schema, user_input, next_step)
|
||||
@@ -515,21 +600,24 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the power management flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input)
|
||||
|
||||
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
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_POWER_DATA_SCHEMA
|
||||
next_step = self.async_step_presence
|
||||
# If comes from async_step_spec_motion
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_power":
|
||||
schema = STEP_CENTRAL_POWER_DATA_SCHEMA
|
||||
elif user_input and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_power
|
||||
else:
|
||||
schema = STEP_POWER_DATA_SCHEMA
|
||||
|
||||
if (
|
||||
user_input
|
||||
and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False
|
||||
):
|
||||
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)
|
||||
|
||||
@@ -541,7 +629,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_power"
|
||||
|
||||
next_step = self.async_step_presence
|
||||
next_step = self.async_step_menu
|
||||
|
||||
# This will return to async_step_power (to keep the "power" step)
|
||||
return await self.generic_step("power", schema, user_input, next_step)
|
||||
@@ -550,25 +638,31 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the presence management flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
|
||||
|
||||
schema = STEP_PRESENCE_DATA_SCHEMA
|
||||
next_step = self.async_step_advanced
|
||||
|
||||
# In Central config -> display the presets_with_ac and goto windows
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
|
||||
next_step = self.async_step_advanced
|
||||
# If comes from async_step_spec_presence
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_presence":
|
||||
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
|
||||
elif user_input and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_presence
|
||||
else:
|
||||
schema = STEP_PRESENCE_DATA_SCHEMA
|
||||
|
||||
if (
|
||||
user_input
|
||||
and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False
|
||||
):
|
||||
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)
|
||||
|
||||
async def async_step_spec_presence(
|
||||
self, user_input: dict | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the specific preseence flow steps"""
|
||||
"""Handle the specific power flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into ConfigFlow.async_step_spec_presence user_input=%s", user_input
|
||||
)
|
||||
@@ -577,26 +671,33 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_presence"
|
||||
|
||||
next_step = self.async_step_advanced
|
||||
next_step = self.async_step_menu
|
||||
|
||||
# This will return to async_step_presence (to keep the "presence" step)
|
||||
# This will return to async_step_power (to keep the "power" step)
|
||||
return await self.generic_step("presence", schema, user_input, next_step)
|
||||
|
||||
async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the advanced parameter flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input)
|
||||
|
||||
schema = STEP_ADVANCED_DATA_SCHEMA
|
||||
next_step = self.async_finalize
|
||||
|
||||
# In Central config -> display the presets_with_ac and goto windows
|
||||
next_step = self.async_step_menu
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
|
||||
# If comes from async_step_spec_presence
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_advanced":
|
||||
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
|
||||
elif user_input and user_input.get(CONF_USE_ADVANCED_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_advanced
|
||||
else:
|
||||
schema = STEP_ADVANCED_DATA_SCHEMA
|
||||
|
||||
if (
|
||||
user_input
|
||||
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)
|
||||
|
||||
@@ -617,22 +718,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
# This will return to async_step_presence (to keep the "presence" step)
|
||||
return await self.generic_step("advanced", schema, user_input, next_step)
|
||||
|
||||
async def async_finalize(self):
|
||||
async def async_step_finalize(self, _):
|
||||
"""Should be implemented by Leaf classes"""
|
||||
raise HomeAssistantError(
|
||||
"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(
|
||||
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
|
||||
@@ -650,7 +741,7 @@ class VersatileThermostatConfigFlow(
|
||||
"""Get options flow for this handler"""
|
||||
return VersatileThermostatOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_finalize(self):
|
||||
async def async_step_finalize(self, _):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
_LOGGER.debug("ConfigFlow.async_finalize")
|
||||
# Removes temporary value
|
||||
@@ -685,155 +776,9 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
CONF_NAME: self._infos[CONF_NAME],
|
||||
}
|
||||
|
||||
return await self.async_step_main(user_input)
|
||||
return await self.async_step_menu(user_input)
|
||||
|
||||
# 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):
|
||||
async def async_step_finalize(self, _):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
if not self._infos[CONF_USE_WINDOW_FEATURE]:
|
||||
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False
|
||||
|
||||
@@ -44,13 +44,18 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
),
|
||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
||||
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_CENTRAL_MODE, default=True): 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,
|
||||
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -61,6 +66,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_MAX, default=35): 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,
|
||||
}
|
||||
)
|
||||
@@ -72,6 +78,7 @@ STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
),
|
||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -96,6 +103,7 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
|
||||
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(
|
||||
[
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
@@ -141,6 +149,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_AUTO_REGULATION_USE_DEVICE_TEMP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -164,6 +173,8 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=10): vol.Coerce(float),
|
||||
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -186,18 +197,6 @@ 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
|
||||
{
|
||||
@@ -245,7 +244,7 @@ STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid
|
||||
|
||||
STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
|
||||
vol.Required(CONF_MOTION_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
@@ -277,10 +276,10 @@ STEP_CENTRAL_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector(
|
||||
vol.Required(CONF_POWER_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
|
||||
vol.Required(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
||||
@@ -295,19 +294,7 @@ STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
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(
|
||||
vol.Required(CONF_PRESENCE_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[
|
||||
PERSON_DOMAIN,
|
||||
@@ -315,7 +302,12 @@ STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
INPUT_BOOLEAN_DOMAIN,
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ from .prop_algorithm import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PRESET_TEMP_SUFFIX = "_temp"
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
|
||||
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
|
||||
@@ -39,11 +40,13 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
|
||||
|
||||
DOMAIN = "versatile_thermostat"
|
||||
|
||||
# The order is important.
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
# Number should be after CLIMATE
|
||||
Platform.NUMBER,
|
||||
Platform.BINARY_SENSOR,
|
||||
]
|
||||
|
||||
@@ -51,6 +54,7 @@ CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
|
||||
CONF_POWER_SENSOR = "power_sensor_entity_id"
|
||||
@@ -105,6 +109,7 @@ CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
|
||||
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
|
||||
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP = "auto_regulation_use_device_temp"
|
||||
CONF_INVERSE_SWITCH = "inverse_switch_command"
|
||||
CONF_AUTO_FAN_MODE = "auto_fan_mode"
|
||||
CONF_AUTO_FAN_NONE = "auto_fan_none"
|
||||
@@ -112,6 +117,7 @@ CONF_AUTO_FAN_LOW = "auto_fan_low"
|
||||
CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
|
||||
CONF_AUTO_FAN_HIGH = "auto_fan_high"
|
||||
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
|
||||
CONF_STEP_TEMPERATURE = "step_temperature"
|
||||
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
@@ -143,7 +149,7 @@ DEFAULT_SHORT_EMA_PARAMS = {
|
||||
}
|
||||
|
||||
CONF_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
@@ -153,7 +159,7 @@ CONF_PRESETS = {
|
||||
}
|
||||
|
||||
CONF_PRESETS_WITH_AC = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
@@ -169,7 +175,7 @@ CONF_PRESETS_WITH_AC = {
|
||||
PRESET_AWAY_SUFFIX = "_away"
|
||||
|
||||
CONF_PRESETS_AWAY = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX,
|
||||
@@ -179,7 +185,7 @@ CONF_PRESETS_AWAY = {
|
||||
}
|
||||
|
||||
CONF_PRESETS_AWAY_WITH_AC = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX,
|
||||
@@ -210,6 +216,7 @@ ALL_CONF = (
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
@@ -254,6 +261,7 @@ ALL_CONF = (
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
|
||||
CONF_INVERSE_SWITCH,
|
||||
CONF_AUTO_FAN_MODE,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG,
|
||||
@@ -270,6 +278,7 @@ ALL_CONF = (
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
CONF_WINDOW_ACTION,
|
||||
CONF_STEP_TEMPERATURE,
|
||||
]
|
||||
+ CONF_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
@@ -355,7 +364,9 @@ CENTRAL_MODES = [
|
||||
class RegulationParamSlow:
|
||||
"""Light parameters for slow latency regulation"""
|
||||
|
||||
kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
kp: float = (
|
||||
0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
)
|
||||
ki: float = (
|
||||
0.8 / 288.0
|
||||
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
@@ -363,7 +374,9 @@ class RegulationParamSlow:
|
||||
1.0 / 25.0
|
||||
) # 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
|
||||
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
|
||||
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
|
||||
)
|
||||
accumulated_error_threshold: float = (
|
||||
2.0 * 288
|
||||
) # this allows up to 2°C long term offset in both directions
|
||||
|
||||
54
custom_components/versatile_thermostat/keep_alive.py
Normal file
54
custom_components/versatile_thermostat/keep_alive.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""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)
|
||||
)
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.1",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -6,24 +6,75 @@ import logging
|
||||
# from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, CoreState # , callback
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberMode
|
||||
from homeassistant.components.number import (
|
||||
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.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
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 (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL,
|
||||
CONF_TEMP_MIN,
|
||||
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,
|
||||
)
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
@@ -40,19 +91,82 @@ async def async_setup_entry(
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL)
|
||||
# is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL)
|
||||
|
||||
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG or not is_central_boiler:
|
||||
return
|
||||
entities = []
|
||||
|
||||
entities = [
|
||||
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
# Creates non central temperature entities
|
||||
if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False):
|
||||
for preset in CONF_PRESETS_VALUES:
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, False, False, entry.data
|
||||
)
|
||||
)
|
||||
if entry.data.get(CONF_AC_MODE, False):
|
||||
for preset in CONF_PRESETS_WITH_AC_VALUES:
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, True, 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):
|
||||
for preset in CONF_PRESETS_AWAY_VALUES:
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, False, True, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
if entry.data.get(CONF_AC_MODE, False):
|
||||
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, True, True, entry.data
|
||||
)
|
||||
)
|
||||
# For central config only
|
||||
else:
|
||||
entities.append(
|
||||
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
|
||||
)
|
||||
for preset in CONF_PRESETS_VALUES:
|
||||
entities.append(
|
||||
CentralConfigTemperatureNumber(
|
||||
hass, unique_id, name, preset, False, False, entry.data
|
||||
)
|
||||
)
|
||||
for preset in CONF_PRESETS_WITH_AC_VALUES:
|
||||
entities.append(
|
||||
CentralConfigTemperatureNumber(
|
||||
hass, unique_id, name, preset, True, False, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
for preset in CONF_PRESETS_AWAY_VALUES:
|
||||
entities.append(
|
||||
CentralConfigTemperatureNumber(
|
||||
hass, unique_id, name, preset, False, True, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
|
||||
entities.append(
|
||||
CentralConfigTemperatureNumber(
|
||||
hass, unique_id, name, preset, True, True, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity):
|
||||
class ActivateBoilerThresholdNumber(
|
||||
NumberEntity, RestoreEntity
|
||||
): # pylint: disable=abstract-method
|
||||
"""Representation of the threshold of the number of VTherm
|
||||
which should be active to activate the boiler"""
|
||||
|
||||
@@ -115,3 +229,239 @@ class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity):
|
||||
|
||||
def __str__(self):
|
||||
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_name}"
|
||||
self._attr_unique_id = f"central_configuration_{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_name}"
|
||||
|
||||
self._attr_unique_id = f"{self._device_name}_{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
|
||||
|
||||
@@ -47,16 +47,16 @@ class PITemperatureRegulator:
|
||||
def set_target_temp(self, target_temp):
|
||||
"""Set the new 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.
|
||||
if self.accumulated_error < 0:
|
||||
self.accumulated_error = 0
|
||||
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
|
||||
# if self.accumulated_error < 0:
|
||||
# self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(
|
||||
self, internal_temp: float, external_temp: float
|
||||
self, room_temp: float, external_temp: float
|
||||
): # pylint: disable=unused-argument
|
||||
"""Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None:
|
||||
if room_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
|
||||
)
|
||||
@@ -68,9 +68,14 @@ class PITemperatureRegulator:
|
||||
return self.target_temp
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = self.target_temp - internal_temp
|
||||
error = self.target_temp - room_temp
|
||||
|
||||
# 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
|
||||
|
||||
# Capping of the error
|
||||
@@ -83,19 +88,12 @@ class PITemperatureRegulator:
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
|
||||
# Calculate the exterior offset
|
||||
# For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp
|
||||
offset_ext = self.k_ext * (internal_temp - external_temp)
|
||||
offset_ext = self.k_ext * (room_temp - external_temp)
|
||||
|
||||
# Capping of offset_ext
|
||||
# Capping of offset
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
# If temperature is near the target_temp, reset the accumulated_error
|
||||
# Issue #199 - don't reset the accumulation error
|
||||
# if abs(error) < self.stabilization_threshold:
|
||||
# _LOGGER.debug("Stabilisation")
|
||||
# self.accumulated_error = 0
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
""" The TPI calculation module """
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROPORTIONAL_FUNCTION_ATAN = "atan"
|
||||
@@ -46,19 +48,20 @@ class PropAlgorithm:
|
||||
|
||||
def calculate(
|
||||
self,
|
||||
target_temp: float,
|
||||
current_temp: float,
|
||||
ext_current_temp: float,
|
||||
cooling=False,
|
||||
target_temp: float | None,
|
||||
current_temp: float | None,
|
||||
ext_current_temp: float | None,
|
||||
hvac_mode: HVACMode,
|
||||
):
|
||||
"""Do the calculation of the duration"""
|
||||
if target_temp is None or current_temp is None:
|
||||
_LOGGER.warning(
|
||||
log = _LOGGER.debug if hvac_mode == HVACMode.OFF else _LOGGER.warning
|
||||
log(
|
||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
else:
|
||||
if cooling:
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
delta_temp = current_temp - target_temp
|
||||
delta_ext_temp = (
|
||||
ext_current_temp
|
||||
|
||||
@@ -14,8 +14,10 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.base_thermostat import (
|
||||
BaseThermostat,
|
||||
ConfigData,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
@@ -57,7 +59,9 @@ async def async_setup_entry(
|
||||
class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
"""Representation of the central mode choice"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the energy sensor"""
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
@@ -67,7 +71,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option = CENTRAL_MODE_AUTO
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
def icon(self) -> str:
|
||||
return "mdi:form-select"
|
||||
|
||||
@property
|
||||
@@ -116,7 +120,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option = option
|
||||
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode=None):
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
# Update all VTherm states
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
@@ -130,5 +134,5 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option, old_central_mode
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -284,7 +284,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Vave open percent"
|
||||
self._attr_name = "Valve open percent"
|
||||
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
|
||||
|
||||
@callback
|
||||
|
||||
@@ -12,24 +12,39 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"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_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"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",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"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"
|
||||
},
|
||||
@@ -37,6 +52,16 @@
|
||||
"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_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
@@ -45,6 +70,7 @@
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
@@ -58,6 +84,7 @@
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
},
|
||||
@@ -66,6 +93,7 @@
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
@@ -77,8 +105,9 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
@@ -99,26 +128,9 @@
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each preset set the target temperature (0 to ignore preset)",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
@@ -184,25 +196,10 @@
|
||||
"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": {
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"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"
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"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"
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -246,6 +243,24 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Main - {name}",
|
||||
"description": "Main mandatory attributes",
|
||||
@@ -257,13 +272,10 @@
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"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_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"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",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"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"
|
||||
},
|
||||
@@ -271,6 +283,16 @@
|
||||
"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_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
@@ -279,6 +301,7 @@
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
@@ -292,6 +315,7 @@
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
},
|
||||
@@ -300,6 +324,7 @@
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
@@ -311,8 +336,9 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
@@ -333,26 +359,9 @@
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets - {name}",
|
||||
"description": "For each preset set the target temperature (0 to ignore preset)",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
@@ -418,25 +427,10 @@
|
||||
"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": {
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"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"
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"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"
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -527,6 +521,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
@@ -16,7 +17,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
|
||||
from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
from .const import (
|
||||
@@ -35,6 +36,7 @@ from .const import (
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
|
||||
CONF_AUTO_FAN_MODE,
|
||||
CONF_AUTO_FAN_NONE,
|
||||
CONF_AUTO_FAN_LOW,
|
||||
@@ -59,19 +61,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class ThermostatOverClimate(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
|
||||
_auto_regulation_mode: str = None
|
||||
_auto_regulation_mode: str | None = None
|
||||
_regulation_algo = None
|
||||
_regulated_target_temp: float = None
|
||||
_auto_regulation_dtemp: float = None
|
||||
_auto_regulation_period_min: int = None
|
||||
_last_regulation_change: datetime = None
|
||||
_regulated_target_temp: float | None = None
|
||||
_auto_regulation_dtemp: float | None = None
|
||||
_auto_regulation_period_min: int | None = None
|
||||
_last_regulation_change: datetime | None = None
|
||||
# The fan mode configured in configEntry
|
||||
_auto_fan_mode: str = None
|
||||
_auto_fan_mode: str | None = None
|
||||
# The current fan mode (could be change by service call)
|
||||
_current_auto_fan_mode: str = None
|
||||
_current_auto_fan_mode: str | None = None
|
||||
# The fan_mode name depending of the current_mode
|
||||
_auto_activated_fan_mode: str = None
|
||||
_auto_deactivated_fan_mode: str = None
|
||||
_auto_activated_fan_mode: str | None = None
|
||||
_auto_deactivated_fan_mode: str | None = None
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
@@ -89,12 +91,15 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
"current_auto_fan_mode",
|
||||
"auto_activated_fan_mode",
|
||||
"auto_deactivated_fan_mode",
|
||||
"auto_regulation_use_device_temp",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the thermostat over switch."""
|
||||
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
@@ -127,7 +132,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return HVACAction.OFF
|
||||
|
||||
@overrides
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
await super()._async_internal_set_temperature(temperature)
|
||||
|
||||
@@ -189,8 +194,42 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
self._last_regulation_change = now
|
||||
for under in self._underlyings:
|
||||
# issue 348 - use device temperature if configured as offset
|
||||
offset_temp = 0
|
||||
device_temp = 0
|
||||
if (
|
||||
# regulation can use the device_temp
|
||||
self.auto_regulation_use_device_temp
|
||||
# and we have access to the device temp
|
||||
and (device_temp := under.underlying_current_temperature) is not None
|
||||
# and target is not reach (ie we need regulation)
|
||||
and (
|
||||
(
|
||||
self.hvac_mode == HVACMode.COOL
|
||||
and self.target_temperature < self.current_temperature
|
||||
)
|
||||
or (
|
||||
self.hvac_mode == HVACMode.HEAT
|
||||
and self.target_temperature > self.current_temperature
|
||||
)
|
||||
)
|
||||
):
|
||||
offset_temp = device_temp - self.current_temperature
|
||||
|
||||
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, self._auto_regulation_dtemp)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f",
|
||||
self,
|
||||
offset_temp,
|
||||
device_temp,
|
||||
target_temp,
|
||||
)
|
||||
|
||||
await under.set_temperature(
|
||||
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
|
||||
target_temp,
|
||||
self._attr_max_temp,
|
||||
self._attr_min_temp,
|
||||
)
|
||||
|
||||
async def _send_auto_fan_mode(self):
|
||||
@@ -239,7 +278,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
@@ -281,7 +320,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode):
|
||||
self._auto_regulation_use_device_temp = config_entry.get(
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
|
||||
@@ -357,7 +400,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0
|
||||
)
|
||||
|
||||
def choose_auto_fan_mode(self, auto_fan_mode):
|
||||
def choose_auto_fan_mode(self, auto_fan_mode: str):
|
||||
"""Choose the correct fan mode depending of the underlying capacities and the configuration"""
|
||||
|
||||
self._current_auto_fan_mode = auto_fan_mode
|
||||
@@ -369,7 +412,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None
|
||||
return
|
||||
|
||||
def find_fan_mode(fan_modes, fan_mode) -> str:
|
||||
def find_fan_mode(fan_modes: list[str], fan_mode: str) -> str | None:
|
||||
"""Return the fan_mode if it exist of None if not"""
|
||||
try:
|
||||
return fan_mode if fan_modes.index(fan_mode) >= 0 else None
|
||||
@@ -427,10 +470,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
# init auto_regulation_mode
|
||||
self.choose_auto_regulation_mode(self._auto_regulation_mode)
|
||||
# Issue 325 - do only once (in post_init and not here)
|
||||
# self.choose_auto_regulation_mode(self._auto_regulation_mode)
|
||||
|
||||
@overrides
|
||||
def restore_specific_previous_state(self, old_state):
|
||||
def restore_specific_previous_state(self, old_state: State):
|
||||
"""Restore my specific attributes from previous state"""
|
||||
old_error = old_state.attributes.get("regulation_accumulated_error")
|
||||
if old_error:
|
||||
@@ -447,9 +491,9 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
|
||||
self._attr_extra_state_attributes[
|
||||
"start_hvac_action_date"
|
||||
] = self._underlying_climate_start_hvac_action_date
|
||||
self._attr_extra_state_attributes["start_hvac_action_date"] = (
|
||||
self._underlying_climate_start_hvac_action_date
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
@@ -465,28 +509,32 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
if self.is_regulated:
|
||||
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
|
||||
self._attr_extra_state_attributes[
|
||||
"regulated_target_temperature"
|
||||
] = self._regulated_target_temp
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_mode"
|
||||
] = self.auto_regulation_mode
|
||||
self._attr_extra_state_attributes[
|
||||
"regulation_accumulated_error"
|
||||
] = self._regulation_algo.accumulated_error
|
||||
self._attr_extra_state_attributes["regulated_target_temperature"] = (
|
||||
self._regulated_target_temp
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_regulation_mode"] = (
|
||||
self.auto_regulation_mode
|
||||
)
|
||||
self._attr_extra_state_attributes["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[
|
||||
"current_auto_fan_mode"
|
||||
] = self._current_auto_fan_mode
|
||||
self._attr_extra_state_attributes["current_auto_fan_mode"] = (
|
||||
self._current_auto_fan_mode
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_activated_fan_mode"
|
||||
] = self._auto_activated_fan_mode
|
||||
self._attr_extra_state_attributes["auto_activated_fan_mode"] = (
|
||||
self._auto_activated_fan_mode
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_deactivated_fan_mode"
|
||||
] = self._auto_deactivated_fan_mode
|
||||
self._attr_extra_state_attributes["auto_deactivated_fan_mode"] = (
|
||||
self._auto_deactivated_fan_mode
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_regulation_use_device_temp"] = (
|
||||
self.auto_regulation_use_device_temp
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -542,7 +590,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_climate_changed(self, event):
|
||||
async def _async_climate_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle unerdlying climate state changes.
|
||||
This method takes the underlying values and update the VTherm with them.
|
||||
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
|
||||
@@ -552,7 +600,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
which is important for feedaback and which cannot generates loops.
|
||||
"""
|
||||
|
||||
async def end_climate_changed(changes):
|
||||
async def end_climate_changed(changes: bool):
|
||||
"""To end the event management"""
|
||||
if changes:
|
||||
self.async_write_ha_state()
|
||||
@@ -745,7 +793,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await end_climate_changed(changes)
|
||||
|
||||
@overrides
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
@@ -757,27 +805,32 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return ret
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self):
|
||||
def auto_regulation_mode(self) -> str | None:
|
||||
"""Get the regulation mode"""
|
||||
return self._auto_regulation_mode
|
||||
|
||||
@property
|
||||
def auto_fan_mode(self):
|
||||
def auto_fan_mode(self) -> str | None:
|
||||
"""Get the auto fan mode"""
|
||||
return self._auto_fan_mode
|
||||
|
||||
@property
|
||||
def regulated_target_temp(self):
|
||||
def auto_regulation_use_device_temp(self) -> bool | None:
|
||||
"""Returns the value of parameter auto_regulation_use_device_temp"""
|
||||
return self._auto_regulation_use_device_temp
|
||||
|
||||
@property
|
||||
def regulated_target_temp(self) -> float | None:
|
||||
"""Get the regulated target temperature"""
|
||||
return self._regulated_target_temp
|
||||
|
||||
@property
|
||||
def is_regulated(self):
|
||||
def is_regulated(self) -> bool:
|
||||
"""Check if the ThermostatOverClimate is regulated"""
|
||||
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""List of available operation modes."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).hvac_modes
|
||||
@@ -850,13 +903,14 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the supported step of target temperature."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_step
|
||||
|
||||
return None
|
||||
# We keep the step configured for the VTherm and not the step of the underlying
|
||||
# @property
|
||||
# def target_temperature_step(self) -> float | None:
|
||||
# """Return the supported step of target temperature."""
|
||||
# if self.underlying_entity(0):
|
||||
# return self.underlying_entity(0).target_temperature_step
|
||||
#
|
||||
# return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
@@ -943,7 +997,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await under.async_turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode: str):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
if fan_mode is None:
|
||||
@@ -976,7 +1030,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._swing_mode = swing_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode):
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_auto_regulation_mode
|
||||
data:
|
||||
@@ -1005,7 +1059,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self._send_regulated_temperature()
|
||||
self.update_custom_attributes()
|
||||
|
||||
async def service_set_auto_fan_mode(self, auto_fan_mode):
|
||||
async def service_set_auto_fan_mode(self, auto_fan_mode: str):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_auto_fan_mode
|
||||
data:
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
@@ -11,11 +15,12 @@ from .const import (
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_INVERSE_SWITCH,
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .underlyings import UnderlyingSwitch
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
@@ -51,7 +56,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
_is_inversed: bool = None
|
||||
_is_inversed: bool | None = None
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
@@ -72,7 +77,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
@@ -101,6 +106,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
thermostat=self,
|
||||
switch_entity_id=switch,
|
||||
initial_delay_sec=idx * delta_cycle,
|
||||
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -121,6 +127,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
switch.startup()
|
||||
|
||||
self.hass.create_task(self.async_control_heating())
|
||||
|
||||
@@ -176,7 +183,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
@@ -200,7 +207,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_switch_changed(self, event):
|
||||
def _async_switch_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle heater switch state changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import (
|
||||
@@ -18,6 +20,9 @@ from .const import (
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
# This is not really self-regulation but regulation here
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
overrides,
|
||||
)
|
||||
|
||||
@@ -26,7 +31,7 @@ from .underlyings import UnderlyingValve
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverValve(BaseThermostat):
|
||||
class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
@@ -44,15 +49,25 @@ class ThermostatOverValve(BaseThermostat):
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, config_entry: ConfigData
|
||||
):
|
||||
"""Initialize the thermostat over switch."""
|
||||
self._valve_open_percent: int = 0
|
||||
self._last_calculation_timestamp: datetime | None = None
|
||||
self._auto_regulation_dpercent: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
|
||||
# Call to super must be done after initialization because it calls post_init at the end
|
||||
super().__init__(hass, unique_id, name, config_entry)
|
||||
|
||||
@property
|
||||
def is_over_valve(self) -> bool:
|
||||
@@ -65,13 +80,25 @@ class ThermostatOverValve(BaseThermostat):
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
return 0
|
||||
else:
|
||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||
return self._valve_open_percent
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
self._auto_regulation_dpercent = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.0
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
@@ -121,7 +148,7 @@ class ThermostatOverValve(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_valve_changed(self, event):
|
||||
async def _async_valve_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle unerdlying valve state changes.
|
||||
This method just log the change. It changes nothing to avoid loops.
|
||||
"""
|
||||
@@ -164,6 +191,17 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self._attr_extra_state_attributes["function"] = self._proportional_function
|
||||
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
|
||||
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_dpercent"
|
||||
] = self._auto_regulation_dpercent
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_period_min"
|
||||
] = self._auto_regulation_period_min
|
||||
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
|
||||
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -177,17 +215,56 @@ class ThermostatOverValve(BaseThermostat):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_LOGGER.debug("%s - recalculate all", self)
|
||||
_LOGGER.debug("%s - recalculate the open percent", self)
|
||||
|
||||
# For testing purpose. Should call _set_now() before
|
||||
now = self.now
|
||||
|
||||
if self._last_calculation_timestamp is not None:
|
||||
period = (now - self._last_calculation_timestamp).total_seconds() / 60
|
||||
if period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
|
||||
self,
|
||||
period,
|
||||
)
|
||||
return
|
||||
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
|
||||
new_valve_percent = round(
|
||||
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
|
||||
)
|
||||
|
||||
dpercent = new_valve_percent - self.valve_open_percent
|
||||
if (
|
||||
dpercent >= -1 * self._auto_regulation_dpercent
|
||||
and dpercent < self._auto_regulation_dpercent
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - do not calculate TPI because regulation_dpercent (%.1f) is not exceeded",
|
||||
self,
|
||||
dpercent,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if self._valve_open_percent == new_valve_percent:
|
||||
_LOGGER.debug("%s - no change in valve_open_percent.", self)
|
||||
return
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -12,24 +12,39 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"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_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"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",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"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"
|
||||
},
|
||||
@@ -37,6 +52,16 @@
|
||||
"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_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
@@ -45,6 +70,7 @@
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
@@ -58,6 +84,7 @@
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
},
|
||||
@@ -66,6 +93,7 @@
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
@@ -77,8 +105,9 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
@@ -99,26 +128,9 @@
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each preset set the target temperature (0 to ignore preset)",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
@@ -184,25 +196,10 @@
|
||||
"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": {
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"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"
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"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"
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -246,6 +243,24 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Main - {name}",
|
||||
"description": "Main mandatory attributes",
|
||||
@@ -257,13 +272,10 @@
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"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_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"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",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"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"
|
||||
},
|
||||
@@ -271,6 +283,16 @@
|
||||
"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_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
@@ -279,6 +301,7 @@
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
@@ -292,6 +315,7 @@
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
},
|
||||
@@ -300,6 +324,7 @@
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
@@ -311,8 +336,9 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
@@ -333,26 +359,9 @@
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets - {name}",
|
||||
"description": "For each preset set the target temperature (0 to ignore preset)",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
@@ -418,25 +427,10 @@
|
||||
"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": {
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"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"
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"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"
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -527,6 +521,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,24 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
@@ -23,13 +41,10 @@
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_window_feature": "Avec détection des ouvertures",
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_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.",
|
||||
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
|
||||
"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."
|
||||
},
|
||||
@@ -37,6 +52,16 @@
|
||||
"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_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entité(s) liée(s)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
@@ -45,6 +70,7 @@
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
@@ -58,6 +84,7 @@
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto ventilation mode"
|
||||
},
|
||||
@@ -66,6 +93,7 @@
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
@@ -77,8 +105,9 @@
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
}
|
||||
@@ -98,27 +127,10 @@
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
|
||||
"title": "Pre-réglages",
|
||||
"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",
|
||||
"data": {
|
||||
"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"
|
||||
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -181,28 +193,13 @@
|
||||
},
|
||||
"presence": {
|
||||
"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'absence.",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"data_description": {
|
||||
"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"
|
||||
"presence_sensor_entity_id": "Id d'entité du capteur de présence"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -258,6 +255,24 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Attributs - {name}",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
@@ -269,13 +284,10 @@
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_window_feature": "Avec détection des ouvertures",
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_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.",
|
||||
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...).",
|
||||
"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."
|
||||
},
|
||||
@@ -283,6 +295,16 @@
|
||||
"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_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entités - {name}",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
@@ -291,6 +313,7 @@
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"heater_keep_alive": "Keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
@@ -304,6 +327,7 @@
|
||||
"auto_regulation_mode": "Auto-regulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
},
|
||||
@@ -312,6 +336,7 @@
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
@@ -323,8 +348,9 @@
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la consigne",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
}
|
||||
@@ -339,26 +365,9 @@
|
||||
},
|
||||
"presets": {
|
||||
"title": "Pre-réglages - {name}",
|
||||
"description": "Réglage des presets. Donnez la température cible (0 pour ignorer le preset)",
|
||||
"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",
|
||||
"data": {
|
||||
"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"
|
||||
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -421,28 +430,13 @@
|
||||
},
|
||||
"presence": {
|
||||
"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'absence.",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"data_description": {
|
||||
"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"
|
||||
"presence_sensor_entity_id": "Id d'entité du capteur de présence"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -545,6 +539,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"heater_entity2_id": "Secondo riscaldatore",
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
@@ -48,6 +49,7 @@
|
||||
"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_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)",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
@@ -191,6 +193,7 @@
|
||||
"heater_entity2_id": "Secondo riscaldatore",
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
@@ -210,6 +213,7 @@
|
||||
"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_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)",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
|
||||
@@ -32,6 +32,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import UnknownEntity, overrides
|
||||
from .keep_alive import IntervalCaller
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -187,6 +188,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
thermostat: Any,
|
||||
switch_entity_id: str,
|
||||
initial_delay_sec: int,
|
||||
keep_alive_sec: int,
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
@@ -202,6 +204,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
self._on_time_sec = 0
|
||||
self._off_time_sec = 0
|
||||
self._hvac_mode = None
|
||||
self._keep_alive = IntervalCaller(hass, keep_alive_sec)
|
||||
|
||||
@property
|
||||
def initial_delay_sec(self):
|
||||
@@ -214,6 +217,11 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Tells if the switch command should be 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
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
@@ -237,35 +245,43 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
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
|
||||
async def turn_off(self):
|
||||
"""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)
|
||||
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
||||
domain = self._entity_id.split(".")[0]
|
||||
# This may fails if called after shutdown
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
command,
|
||||
data,
|
||||
)
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(domain, command, data)
|
||||
self._keep_alive.set_async_action(self._keep_alive_callback)
|
||||
except Exception:
|
||||
self._keep_alive.cancel()
|
||||
raise
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
async def turn_on(self):
|
||||
"""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)
|
||||
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
||||
domain = self._entity_id.split(".")[0]
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
command,
|
||||
data,
|
||||
)
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(domain, command, data)
|
||||
self._keep_alive.set_async_action(self._keep_alive_callback)
|
||||
except Exception:
|
||||
self._keep_alive.cancel()
|
||||
raise
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
@@ -422,6 +438,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
def remove_entity(self):
|
||||
"""Remove the entity after stopping its cycle"""
|
||||
self._cancel_cycle()
|
||||
self._keep_alive.cancel()
|
||||
|
||||
|
||||
class UnderlyingClimate(UnderlyingEntity):
|
||||
@@ -567,6 +584,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
"""Set the target temperature"""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": self.cap_sent_value(temperature),
|
||||
@@ -671,6 +689,18 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return False
|
||||
return self._underlying_climate.is_aux_heat
|
||||
|
||||
@property
|
||||
def underlying_current_temperature(self) -> float | None:
|
||||
"""Get the underlying current_temperature if it exists
|
||||
and if initialized"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
|
||||
if not hasattr(self._underlying_climate, "current_temperature"):
|
||||
return None
|
||||
|
||||
return self._underlying_climate.current_temperature
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if not self.is_initialized:
|
||||
@@ -765,7 +795,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
await self.send_percent_open()
|
||||
|
||||
async def turn_on(self):
|
||||
"""Nothing to do for Valve because it cannot be turned off"""
|
||||
"""Nothing to do for Valve because it cannot be turned on"""
|
||||
self.set_valve_open_percent()
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
""" The API of Versatile Thermostat"""
|
||||
|
||||
import logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 (
|
||||
DOMAIN,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
@@ -51,19 +56,24 @@ class VersatileThermostatAPI(dict):
|
||||
self._central_boiler_entity = None
|
||||
self._threshold_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):
|
||||
"""Search for a central configuration"""
|
||||
for config_entry in VersatileThermostatAPI._hass.config_entries.async_entries(
|
||||
DOMAIN
|
||||
):
|
||||
if (
|
||||
config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
== CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
):
|
||||
central_config = config_entry
|
||||
return central_config
|
||||
return None
|
||||
if not self._central_configuration:
|
||||
for (
|
||||
config_entry
|
||||
) in VersatileThermostatAPI._hass.config_entries.async_entries(DOMAIN):
|
||||
if (
|
||||
config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
== CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
):
|
||||
self._central_configuration = config_entry
|
||||
break
|
||||
# return self._central_configuration
|
||||
return self._central_configuration
|
||||
|
||||
def add_entry(self, entry: ConfigEntry):
|
||||
"""Add a new entry"""
|
||||
@@ -106,10 +116,64 @@ class VersatileThermostatAPI(dict):
|
||||
):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
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):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
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):
|
||||
"""Reload the central boiler list of entities if a central boiler is used"""
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 50 KiB |
BIN
images/config-use-internal-temp.png
Normal file
BIN
images/config-use-internal-temp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
images/en/config-linked-entity.png
Normal file
BIN
images/en/config-linked-entity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
0
pyproject.toml
Normal file
0
pyproject.toml
Normal file
@@ -1,2 +1 @@
|
||||
homeassistant==2023.12.1
|
||||
ffmpeg
|
||||
homeassistant==2024.2.1
|
||||
|
||||
@@ -25,5 +25,9 @@ fi
|
||||
## without resulting to symlinks.
|
||||
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
|
||||
hass --config "${PWD}/config" --debug
|
||||
@@ -1,9 +1,9 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method
|
||||
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch, MagicMock # pylint: disable=unused-import
|
||||
import pytest # pylint: disable=unused-import
|
||||
|
||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||
@@ -23,9 +23,7 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
)
|
||||
from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
@@ -51,6 +49,7 @@ from .const import ( # pylint: disable=unused-import
|
||||
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||
@@ -71,6 +70,12 @@ from .const import ( # pylint: disable=unused-import
|
||||
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 = (
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
@@ -79,6 +84,7 @@ FULL_SWITCH_CONFIG = (
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_FULL_FEATURES
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
@@ -93,6 +99,7 @@ FULL_SWITCH_AC_CONFIG = (
|
||||
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_AC_CONFIG
|
||||
| MOCK_FULL_FEATURES
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
@@ -100,7 +107,6 @@ FULL_SWITCH_AC_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
@@ -110,6 +116,15 @@ PARTIAL_CLIMATE_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
@@ -146,6 +161,7 @@ FULL_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,
|
||||
"frost_temp": 10,
|
||||
@@ -172,6 +188,7 @@ FULL_CENTRAL_CONFIG = {
|
||||
CONF_NO_MOTION_PRESET: "frost",
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
CONF_PRESET_POWER: 14,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 11,
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
@@ -186,6 +203,7 @@ FULL_CENTRAL_CONFIG_WITH_BOILER = {
|
||||
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,
|
||||
"frost_temp": 10,
|
||||
@@ -263,6 +281,7 @@ class MockClimate(ClimateEntity):
|
||||
self._attr_target_temperature = 20
|
||||
self._attr_current_temperature = 15
|
||||
self._attr_hvac_action = hvac_action
|
||||
self._attr_target_temperature_step = 0.2
|
||||
self._fan_modes = fan_modes if fan_modes else None
|
||||
self._attr_fan_mode = None
|
||||
|
||||
@@ -310,6 +329,10 @@ class MockClimate(ClimateEntity):
|
||||
"""Set the HVACaction"""
|
||||
self._attr_hvac_action = hvac_action
|
||||
|
||||
def set_current_temperature(self, current_temperature):
|
||||
"""Set the current_temperature"""
|
||||
self._attr_current_temperature = current_temperature
|
||||
|
||||
|
||||
class MockUnavailableClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
@@ -476,14 +499,18 @@ async def create_thermostat(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||
) -> BaseThermostat:
|
||||
"""Creates and return a TPI Thermostat"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
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
|
||||
|
||||
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
# We should reload the VTherm links
|
||||
# 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
|
||||
@@ -506,11 +533,14 @@ async def create_central_config( # pylint: disable=dangerous-default-value
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
return central_configuration
|
||||
|
||||
|
||||
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
|
||||
"""Search and return the entity in the domain"""
|
||||
component = hass.data[domain]
|
||||
for entity in component.entities:
|
||||
_LOGGER.debug("Found %s entity: %s", domain, entity.entity_id)
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
return None
|
||||
@@ -830,3 +860,25 @@ def cancel_switchs_cycles(entity: BaseThermostat):
|
||||
return
|
||||
for under in entity._underlyings:
|
||||
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]
|
||||
+ "_"
|
||||
+ 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)
|
||||
|
||||
@@ -35,8 +35,31 @@ from .commons import (
|
||||
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
|
||||
|
||||
# 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.
|
||||
# Remove to enable selective use of this fixture
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" The commons const for all tests """
|
||||
|
||||
from homeassistant.components.climate.const import ( # pylint: disable=unused-import
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
@@ -18,10 +19,10 @@ MOCK_TH_OVER_SWITCH_MAIN_CONFIG = {
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
# CONF_USE_WINDOW_FEATURE: True,
|
||||
# CONF_USE_MOTION_FEATURE: True,
|
||||
# CONF_USE_POWER_FEATURE: True,
|
||||
# CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
@@ -51,7 +53,7 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: True
|
||||
CONF_USE_CENTRAL_MODE: True,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -66,11 +69,13 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
|
||||
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
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
@@ -88,6 +93,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER_2: "switch.mock_4switch1",
|
||||
CONF_HEATER_3: "switch.mock_4switch2",
|
||||
CONF_HEATER_4: "switch.mock_4switch3",
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
@@ -105,6 +111,17 @@ MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
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,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.1,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
||||
@@ -121,21 +138,23 @@ MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 1,
|
||||
}
|
||||
|
||||
# TODO remove this later
|
||||
MOCK_PRESETS_CONFIG = {
|
||||
PRESET_FROST_PROTECTION + "_temp": 7,
|
||||
PRESET_ECO + "_temp": 16,
|
||||
PRESET_COMFORT + "_temp": 17,
|
||||
PRESET_BOOST + "_temp": 18,
|
||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
|
||||
PRESET_ECO + PRESET_TEMP_SUFFIX: 16,
|
||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 17,
|
||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: 18,
|
||||
}
|
||||
|
||||
# TODO remove this later
|
||||
MOCK_PRESETS_AC_CONFIG = {
|
||||
PRESET_FROST_PROTECTION + "_temp": 7,
|
||||
PRESET_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 20,
|
||||
PRESET_ECO + "_ac_temp": 25,
|
||||
PRESET_COMFORT + "_ac_temp": 23,
|
||||
PRESET_BOOST + "_ac_temp": 21,
|
||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
|
||||
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
|
||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
|
||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: 20,
|
||||
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 25,
|
||||
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 23,
|
||||
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 21,
|
||||
}
|
||||
|
||||
MOCK_WINDOW_CONFIG = {
|
||||
@@ -171,20 +190,10 @@ MOCK_POWER_CONFIG = {
|
||||
|
||||
MOCK_PRESENCE_CONFIG = {
|
||||
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 = {
|
||||
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 = {
|
||||
|
||||
@@ -211,13 +211,14 @@ async def test_over_climate_auto_fan_mode_turbo_activation(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity: ThermostatOverClimate = search_entity(
|
||||
hass, "climate.theoverclimatemockname", "climate"
|
||||
)
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# entity: ThermostatOverClimate = search_entity(
|
||||
# hass, "climate.theoverclimatemockname", "climate"
|
||||
# )
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from unittest.mock import patch # , call
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -52,18 +52,19 @@ async def test_over_climate_regulation(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -71,6 +72,7 @@ async def test_over_climate_regulation(
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
assert entity.auto_regulation_use_device_temp is False
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
@@ -126,9 +128,7 @@ async def test_over_climate_regulation(
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert (
|
||||
entity.regulated_target_temp == 18 - 2
|
||||
) # normally 0.6 but round_to_nearest gives 0.5
|
||||
assert entity.regulated_target_temp == 18 - 2.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -162,18 +162,19 @@ async def test_over_climate_regulation_ac_mode(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -374,3 +375,172 @@ async def test_over_climate_regulation_limitations(
|
||||
assert (
|
||||
entity.regulated_target_temp == 17 + 1.5
|
||||
) # 0.7 without round_to_nearest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation
|
||||
data=PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP | {CONF_AUTO_REGULATION_DTEMP: 0.5},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
assert fake_underlying_climate.current_temperature == 15
|
||||
|
||||
# Creates the regulated VTherm over climate
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity: ThermostatOverClimate = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
assert entity.auto_regulation_use_device_temp is True
|
||||
|
||||
# 1. Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.regulated_target_temp == entity.min_temp
|
||||
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# 2. set manual target temp (at now - 7) -> no regulation should occurs
|
||||
# room temp is 18
|
||||
# target is 16
|
||||
# internal heater temp is 15
|
||||
fake_underlying_climate.set_current_temperature(15)
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await entity.async_set_temperature(temperature=16)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be higher
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
# The calcul is the following: 16 + (16 - 18) x 0.4 (strong) + 0 x ki - 1 (device offset)
|
||||
assert (
|
||||
entity.regulated_target_temp == 15
|
||||
) # round(16 + (16 - 18) * 0.4 + 0 * 0.08)
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
# because device offset is -3 but not used because target is reach
|
||||
"temperature": 15.0,
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 3. change temperature so that the regulated temperature should slow down
|
||||
# HVACMODE.HEAT
|
||||
# room temp is 15
|
||||
# target is 18
|
||||
# internal heater temp is 20
|
||||
fake_underlying_climate.set_current_temperature(20.1)
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under (device offset is -2)
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 19.5 # round(18 + 1.4, 0.5)
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 24.5, # round(19.5 + 5, 0.5)
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 4. In cool mode
|
||||
# room temp is 25
|
||||
# target is 23
|
||||
# internal heater temp is 27
|
||||
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||
await entity.async_set_temperature(temperature=23)
|
||||
fake_underlying_climate.set_current_temperature(26.9)
|
||||
await send_ext_temperature_change_event(entity, 30, event_timestamp)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(entity, 25, event_timestamp)
|
||||
|
||||
# the regulated temperature should be upper (device offset is +1.9)
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 22.5
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 24.5, # round(22.5 + 1.9° of offset)
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -380,18 +380,19 @@ async def test_bug_82(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -490,18 +491,19 @@ async def test_bug_101(
|
||||
) as mock_find_climate, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -591,6 +593,7 @@ async def test_bug_272(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# default value are min 15°, max 30°, step 0.1
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
@@ -605,24 +608,27 @@ async def test_bug_272(
|
||||
), patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# The VTherm value and not the underlying value
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.is_regulated is True
|
||||
|
||||
|
||||
@@ -156,21 +156,30 @@ async def test_update_central_boiler_state_simple(
|
||||
await switch1.async_turn_on()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
assert mock_service_call.call_count == 2
|
||||
|
||||
# Sometimes this test fails
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
]
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
@@ -760,7 +769,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
climate1.set_hvac_action(HVACAction.HEATING)
|
||||
climate1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
@@ -801,7 +810,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
climate1.set_hvac_action(HVACAction.IDLE)
|
||||
climate1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
|
||||
@@ -4,23 +4,13 @@
|
||||
from unittest.mock import patch # , call
|
||||
|
||||
# from datetime import datetime # , timedelta
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
# 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 homeassistant.config_entries import SOURCE_USER
|
||||
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 (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
@@ -31,8 +21,8 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Tests the clean_central_config_doubon of base_thermostat"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
@@ -80,13 +70,16 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
},
|
||||
)
|
||||
|
||||
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"
|
||||
entity = await create_thermostat(
|
||||
hass, central_config_entry, "climate.thecentralconfigmockname"
|
||||
)
|
||||
# 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
|
||||
|
||||
@@ -101,12 +94,12 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
assert api.nb_active_device_for_boiler_entity is None
|
||||
assert api.nb_active_device_for_boiler is None
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is None
|
||||
assert api.nb_active_device_for_boiler_threshold is None
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_minimal_over_switch_wo_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||
):
|
||||
@@ -124,6 +117,7 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
@@ -165,8 +159,9 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity._attr_min_temp == 8
|
||||
assert entity._attr_max_temp == 18
|
||||
assert entity.min_temp == 8
|
||||
assert entity.max_temp == 18
|
||||
assert entity.target_temperature_step == 0.3
|
||||
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
|
||||
assert entity.is_window_auto_enabled is False
|
||||
assert entity.nb_underlying_entities == 1
|
||||
@@ -183,8 +178,8 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_full_over_switch_wo_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||
):
|
||||
@@ -202,6 +197,7 @@ async def test_full_over_switch_wo_central_config(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
@@ -257,8 +253,9 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity._attr_min_temp == 8
|
||||
assert entity._attr_max_temp == 18
|
||||
assert entity.min_temp == 8
|
||||
assert entity.max_temp == 18
|
||||
assert entity.target_temperature_step == 0.3
|
||||
assert entity.preset_modes == [
|
||||
"none",
|
||||
"frost",
|
||||
@@ -299,8 +296,8 @@ async def test_full_over_switch_wo_central_config(
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_full_over_switch_with_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -318,6 +315,7 @@ async def test_full_over_switch_with_central_config(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
@@ -369,8 +367,9 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity._attr_min_temp == 15
|
||||
assert entity._attr_max_temp == 30
|
||||
assert entity.min_temp == 15
|
||||
assert entity.max_temp == 30
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.preset_modes == [
|
||||
"none",
|
||||
"frost",
|
||||
@@ -431,7 +430,13 @@ async def test_over_switch_with_central_config_but_no_central_config(
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
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["errors"] == {}
|
||||
|
||||
@@ -442,15 +447,11 @@ async def test_over_switch_with_central_config_but_no_central_config(
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
# in case of error we stays in main
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {"use_main_central_config": "no_central_config"}
|
||||
|
||||
@@ -170,6 +170,8 @@ async def test_config_with_central_mode_none(
|
||||
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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -310,6 +312,8 @@ async def test_switch_change_central_mode_true(
|
||||
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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -444,6 +448,8 @@ async def test_switch_ac_change_central_mode_true(
|
||||
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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -577,6 +583,8 @@ async def test_climate_ac_change_central_mode_false(
|
||||
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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -734,6 +742,8 @@ async def test_climate_ac_only_change_central_mode_true(
|
||||
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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -889,6 +899,8 @@ async def test_switch_change_central_mode_true_with_window(
|
||||
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(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
""" Test the Versatile Thermostat config flow """
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
||||
|
||||
@@ -19,14 +20,14 @@ async def test_show_form(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# 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(
|
||||
hass: HomeAssistant, skip_hass_states_get, init_central_config
|
||||
): # pylint: disable=unused-argument
|
||||
@@ -34,49 +35,145 @@ async def test_user_config_flow_over_switch(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"type",
|
||||
"features",
|
||||
"presets",
|
||||
"advanced",
|
||||
]
|
||||
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": "main"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_MAIN_CONFIG
|
||||
result["flow_id"],
|
||||
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
|
||||
|
||||
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": "type"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
result["flow_id"],
|
||||
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",
|
||||
"type",
|
||||
"features",
|
||||
"tpi",
|
||||
"presets",
|
||||
"advanced",
|
||||
"finalize", # because by default all options are "use central config"
|
||||
]
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result.get("errors") is None
|
||||
|
||||
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["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
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
|
||||
|
||||
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": "presets"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
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
|
||||
|
||||
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": "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",
|
||||
"type",
|
||||
"features",
|
||||
"tpi",
|
||||
"presets",
|
||||
"window",
|
||||
"motion",
|
||||
"power",
|
||||
"presence",
|
||||
"advanced",
|
||||
# "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["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -85,10 +182,16 @@ async def test_user_config_flow_over_switch(
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
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": "motion"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "motion"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -98,42 +201,74 @@ async def test_user_config_flow_over_switch(
|
||||
},
|
||||
)
|
||||
|
||||
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": "power"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "power"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
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",
|
||||
"type",
|
||||
"features",
|
||||
"tpi",
|
||||
"presets",
|
||||
"window",
|
||||
"motion",
|
||||
"power",
|
||||
"presence",
|
||||
"advanced",
|
||||
"finalize",
|
||||
]
|
||||
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": "presence"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "presence"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
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["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "finalize"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("errors") is None
|
||||
assert result["data"] == (
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"}
|
||||
| {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"}
|
||||
| {CONF_PRESENCE_SENSOR: "person.presence_sensor"}
|
||||
# | {CONF_PRESENCE_SENSOR: "person.presence_sensor"} now in central config
|
||||
| {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
@@ -145,6 +280,10 @@ async def test_user_config_flow_over_switch(
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USE_CENTRAL_MODE: True,
|
||||
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,
|
||||
}
|
||||
)
|
||||
assert result["result"]
|
||||
@@ -156,86 +295,205 @@ async def test_user_config_flow_over_switch(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [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(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||
await create_central_config(hass)
|
||||
"""Test the config flow with all thermostat_over_switch features and never use central config.
|
||||
We don't use any features"""
|
||||
# await create_central_config(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"type",
|
||||
"features",
|
||||
"presets",
|
||||
"advanced",
|
||||
]
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
result["flow_id"], user_input={"next_step_id": "main"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
result["flow_id"],
|
||||
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["step_id"] == "main"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
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(
|
||||
result["flow_id"], user_input={"next_step_id": "type"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
result["flow_id"],
|
||||
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",
|
||||
"type",
|
||||
"features",
|
||||
"presets",
|
||||
"advanced",
|
||||
# "finalize", # because we need Advanced default parameters
|
||||
]
|
||||
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": "presets"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
result["flow_id"], user_input={"next_step_id": "features"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "features"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
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",
|
||||
"type",
|
||||
"features",
|
||||
"presets",
|
||||
"advanced",
|
||||
# "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["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False}
|
||||
result["flow_id"],
|
||||
user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
result["flow_id"],
|
||||
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",
|
||||
"type",
|
||||
"features",
|
||||
"presets",
|
||||
"advanced",
|
||||
"finalize", # Now finalize is present
|
||||
]
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "finalize"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("errors") is None
|
||||
assert result[
|
||||
"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_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | MOCK_DEFAULT_FEATURE_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 | {
|
||||
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_TPI_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_TPI_CENTRAL_CONFIG: False,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG: False,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
@@ -252,6 +510,8 @@ async def test_user_config_flow_over_climate(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# TODO reimplement this
|
||||
@pytest.mark.skip
|
||||
async def test_user_config_flow_window_auto_ok(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_get,
|
||||
@@ -264,7 +524,7 @@ async def test_user_config_flow_window_auto_ok(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@@ -274,9 +534,9 @@ async def test_user_config_flow_window_auto_ok(
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -294,59 +554,59 @@ async def test_user_config_flow_window_auto_ok(
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_WINDOW_AUTO_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
|
||||
@@ -388,6 +648,8 @@ async def test_user_config_flow_window_auto_ok(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# TODO reimplement this
|
||||
@pytest.mark.skip
|
||||
async def test_user_config_flow_window_auto_ko(
|
||||
hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument
|
||||
):
|
||||
@@ -399,7 +661,7 @@ async def test_user_config_flow_window_auto_ko(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@@ -409,9 +671,9 @@ async def test_user_config_flow_window_auto_ko(
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -428,41 +690,41 @@ async def test_user_config_flow_window_auto_ko(
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -472,9 +734,9 @@ async def test_user_config_flow_window_auto_ko(
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -483,9 +745,9 @@ async def test_user_config_flow_window_auto_ko(
|
||||
|
||||
# Since issue #280 we cannot have the error because we only display the
|
||||
# MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
# We should stay on window with an error
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
# "window_sensor_entity_id": "window_open_detection_method"
|
||||
# }
|
||||
assert result["step_id"] == "advanced"
|
||||
@@ -493,6 +755,8 @@ async def test_user_config_flow_window_auto_ko(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# TODO reimplement this
|
||||
@pytest.mark.skip
|
||||
async def test_user_config_flow_over_4_switches(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_get,
|
||||
@@ -502,11 +766,11 @@ async def test_user_config_flow_over_4_switches(
|
||||
|
||||
await create_central_config(hass)
|
||||
|
||||
SOURCE_CONFIG = {
|
||||
SOURCE_CONFIG = { # pylint: disable=invalid-name
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
}
|
||||
|
||||
MAIN_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
MAIN_CONFIG = { # pylint: disable=invalid-name
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
@@ -520,11 +784,12 @@ async def test_user_config_flow_over_4_switches(
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
}
|
||||
|
||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
TYPE_CONFIG = { # pylint: disable=invalid-name
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
@@ -534,7 +799,7 @@ async def test_user_config_flow_over_4_switches(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@@ -542,43 +807,43 @@ async def test_user_config_flow_over_4_switches(
|
||||
user_input=SOURCE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MAIN_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=TYPE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
assert result.get("errors") is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
|
||||
|
||||
@@ -260,6 +260,7 @@ async def test_multiple_switchs(
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
@@ -746,6 +747,7 @@ async def test_multiple_switch_power_management(
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
|
||||
@@ -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(19, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # error change sign
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.0
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # =
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20 # =
|
||||
|
||||
|
||||
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(19, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # Error sign change
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # =
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
|
||||
|
||||
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(19, 10) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.7
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.0 # error sign change
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.1
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 22.1
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 22.9
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 23.0
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 23.0
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 23.0
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 21.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 22.1
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 22.9
|
||||
|
||||
# to reset the accumulated erro
|
||||
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(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 23.3
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 23.1
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.1
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.3 # error sign change
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 22.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 23.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 23.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 23.1
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 23.5
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 24
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@ from unittest.mock import patch, call
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
|
||||
@@ -38,18 +34,7 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity: BaseThermostat = find_my_entity("climate.theoverswitchmockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverswitchmockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverSwitch)
|
||||
@@ -108,18 +93,19 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -174,23 +160,24 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname")
|
||||
# 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.theover4switchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOver4SwitchMockName"
|
||||
assert entity.is_over_climate is False
|
||||
assert entity.is_over_switch
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
@@ -264,6 +251,7 @@ async def test_over_switch_deactivate_preset(
|
||||
CONF_HEATER_2: None,
|
||||
CONF_HEATER_3: None,
|
||||
CONF_HEATER_4: None,
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_SECURITY_DELAY_MIN: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
},
|
||||
|
||||
@@ -56,6 +56,23 @@ async def test_over_switch_ac_full_start(
|
||||
assert entity
|
||||
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.is_over_climate is False # pylint: disable=protected-access
|
||||
assert entity.ac_mode is True
|
||||
|
||||
258
tests/test_switch_keep_alive.py
Normal file
258
tests/test_switch_keep_alive.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""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"}),
|
||||
],
|
||||
)
|
||||
1142
tests/test_temp_number.py
Normal file
1142
tests/test_temp_number.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
""" Test the TPI algorithm """
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@@ -42,53 +45,54 @@ async def test_tpi_calculation(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity._prop_algorithm # pylint: disable=protected-access
|
||||
|
||||
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access
|
||||
tpi_algo: PropAlgorithm = entity._prop_algorithm # pylint: disable=protected-access
|
||||
assert tpi_algo
|
||||
|
||||
tpi_algo.calculate(15, 10, 7)
|
||||
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
|
||||
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
|
||||
assert entity.mean_cycle_power is None # no device power configured
|
||||
|
||||
tpi_algo.calculate(15, 14, 5, False)
|
||||
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.4
|
||||
assert tpi_algo.calculated_on_percent == 0.4
|
||||
assert tpi_algo.on_time_sec == 120
|
||||
assert tpi_algo.off_time_sec == 180
|
||||
|
||||
tpi_algo.set_security(0.1)
|
||||
tpi_algo.calculate(15, 14, 5, False)
|
||||
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.1
|
||||
assert tpi_algo.calculated_on_percent == 0.4
|
||||
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
||||
assert tpi_algo.off_time_sec == 270
|
||||
|
||||
tpi_algo.unset_security()
|
||||
tpi_algo.calculate(15, 14, 5, False)
|
||||
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.4
|
||||
assert tpi_algo.calculated_on_percent == 0.4
|
||||
assert tpi_algo.on_time_sec == 120
|
||||
assert tpi_algo.off_time_sec == 180
|
||||
|
||||
# Test minimal activation delay
|
||||
tpi_algo.calculate(15, 14.7, 15, False)
|
||||
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 0.09
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
tpi_algo.set_security(0.09)
|
||||
tpi_algo.calculate(15, 14.7, 15, False)
|
||||
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 0.09
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
tpi_algo.unset_security()
|
||||
tpi_algo.calculate(25, 30, 35, True)
|
||||
tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
|
||||
assert tpi_algo.on_percent == 1
|
||||
assert tpi_algo.calculated_on_percent == 1
|
||||
assert tpi_algo.on_time_sec == 300
|
||||
@@ -96,9 +100,24 @@ async def test_tpi_calculation(
|
||||
assert entity.mean_cycle_power is None # no device power configured
|
||||
|
||||
tpi_algo.set_security(0.09)
|
||||
tpi_algo.calculate(25, 30, 35, True)
|
||||
tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 1
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=line-too-long, disable=protected-access
|
||||
|
||||
""" Test the normal start of a Switch AC Thermostat """
|
||||
from unittest.mock import patch, call
|
||||
@@ -37,10 +37,10 @@ async def test_over_valve_full_start(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
PRESET_FROST_PROTECTION + "_temp": 7,
|
||||
PRESET_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 21,
|
||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
|
||||
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
|
||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
|
||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_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_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3,
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.2,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.3,
|
||||
CONF_PRESET_POWER: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
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
|
||||
|
||||
# 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(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
@@ -196,15 +196,18 @@ async def test_over_valve_full_start(
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 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)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
assert entity.target_temperature == 17.2
|
||||
assert entity.target_temperature == 17.2 # Comfort with presence off
|
||||
assert entity.valve_open_percent == 73
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Change presence to on
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state == STATE_ON # pylint: disable=protected-access
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
@@ -225,7 +228,7 @@ async def test_over_valve_full_start(
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get", return_value=expected_state
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_temperature_change_event(entity, 20, datetime.now())
|
||||
assert entity.valve_open_percent == 0
|
||||
assert entity.is_device_active is True # Should be 0 but in fact 10 is send
|
||||
@@ -275,7 +278,7 @@ async def test_over_valve_full_start(
|
||||
assert entity.valve_open_percent == 7
|
||||
|
||||
# Unset the presence
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state == STATE_OFF # pylint: disable=protected-access
|
||||
assert entity.valve_open_percent == 10
|
||||
@@ -324,3 +327,232 @@ async def test_over_valve_full_start(
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
assert entity.target_temperature == 17.1 # eco
|
||||
assert entity.valve_open_percent == 10
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_valve_regulation(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverValveMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverValveMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_VALVE: "number.mock_valve",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
|
||||
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
|
||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
|
||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
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: 60,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
# only send new valve open percent if dtemp is > 30%
|
||||
CONF_AUTO_REGULATION_DTEMP: 5,
|
||||
# only send new valve open percent last mesure was more than 5 min ago
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 5,
|
||||
},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 1. prepare the Valve at now
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theovervalvemockname"
|
||||
)
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverValve)
|
||||
|
||||
assert entity.name == "TheOverValveMockName"
|
||||
assert entity.is_over_valve is True
|
||||
assert entity._auto_regulation_dpercent == 5
|
||||
assert entity._auto_regulation_period_min == 5
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
# 2. Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature
|
||||
# at now +1
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No heating now
|
||||
assert entity.valve_open_percent == 0
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 3. Set the preset
|
||||
# at now +1
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
# set preset
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.preset_mode == PRESET_BOOST
|
||||
assert entity.target_temperature == 21
|
||||
# the preset have changed
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.PRESET_EVENT,
|
||||
{"preset": PRESET_BOOST},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# Still no heating because we don't have temperature
|
||||
assert entity.valve_open_percent == 0
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
# 4. Set temperature and external temperature
|
||||
# at now + 1 (but the _last_calculation_timestamp is still not send)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="90"),
|
||||
):
|
||||
# Change temperature
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_temperature_change_event(entity, 18, now)
|
||||
assert entity.valve_open_percent == 90
|
||||
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 90},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 5. Set external temperature
|
||||
# at now + 1
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="90"),
|
||||
):
|
||||
# Change external temperature
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_ext_temperature_change_event(entity, 10, now)
|
||||
|
||||
# Should not have change due to regulation (period_min !)
|
||||
assert entity.valve_open_percent == 90
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 6. Set temperature
|
||||
# at now + 5 (to avoid the period_min threshold)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="90"),
|
||||
):
|
||||
# Change external temperature
|
||||
now = now + timedelta(minutes=5)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_ext_temperature_change_event(entity, 15, now)
|
||||
|
||||
# Should have change this time to 96
|
||||
assert entity.valve_open_percent == 96
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 96},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 7. Set small temperature update to test dtemp threshold
|
||||
# at now + 5
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="96"),
|
||||
):
|
||||
# Change external temperature
|
||||
now = now + timedelta(minutes=5)
|
||||
entity._set_now(now)
|
||||
|
||||
# this generate a delta percent of -3
|
||||
await send_temperature_change_event(entity, 18.1, now)
|
||||
|
||||
# Should not have due to dtemp
|
||||
assert entity.valve_open_percent == 96
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
Reference in New Issue
Block a user