Compare commits
27 Commits
5.1.0.beta
...
5.4.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76382ebb35 | ||
|
|
90f9a0e1e3 | ||
|
|
ed977b53cd | ||
|
|
5d453393f8 | ||
|
|
d2f2ab7804 | ||
|
|
b0b6d0478d | ||
|
|
f8a2c9baa9 | ||
|
|
8cbd81012c | ||
|
|
26844593b1 | ||
|
|
c12a91a5ff | ||
|
|
3da271b671 | ||
|
|
e8bb465b43 | ||
|
|
d7ec6770c4 | ||
|
|
51428aa875 | ||
|
|
6ea6fe8542 | ||
|
|
a18d10fa3f | ||
|
|
7d4ee40b4d | ||
|
|
1aaf9c8c8e | ||
|
|
ae93a8b97c | ||
|
|
cbe98ae20c | ||
|
|
bfcc854c3e | ||
|
|
683aa050f3 | ||
|
|
7476e7fa64 | ||
|
|
c222feda1a | ||
|
|
d05df021ab | ||
|
|
27a267139f | ||
|
|
707f40d406 |
@@ -1,5 +1,8 @@
|
||||
default_config:
|
||||
|
||||
# ffmeg
|
||||
ffmpeg:
|
||||
|
||||
logger:
|
||||
default: info
|
||||
logs:
|
||||
@@ -25,6 +28,8 @@ versatile_thermostat:
|
||||
max_alpha: 0.6
|
||||
halflife_sec: 301
|
||||
precision: 3
|
||||
safety_mode:
|
||||
check_outdoor_sensor: false
|
||||
|
||||
input_number:
|
||||
fake_temperature_sensor1:
|
||||
@@ -63,6 +68,13 @@ input_number:
|
||||
max: 90
|
||||
icon: mdi:pipe-valve
|
||||
unit_of_measurement: percentage
|
||||
fake_boiler_temperature:
|
||||
name: Central thermostat temp
|
||||
min: 0
|
||||
max: 30
|
||||
icon: mdi:thermostat
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
|
||||
input_boolean:
|
||||
# input_boolean to simulate the windows entity. Only for development environment.
|
||||
@@ -158,6 +170,7 @@ climate:
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
|
||||
recorder:
|
||||
commit_interval: 1
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
@@ -165,6 +178,7 @@ recorder:
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"waderyan.gitblame",
|
||||
"keesschollaart.vscode-home-assistant",
|
||||
"vscode.markdown-math",
|
||||
"yzhang.markdown-all-in-one"
|
||||
"yzhang.markdown-all-in-one",
|
||||
"github.vscode-github-actions"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -4,6 +4,8 @@ about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
|
||||
|
||||
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
|
||||
|
||||
If you have a simple question or you are not sure this is an issue, don't open an issue but open a new discussion [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
|
||||
@@ -13,7 +15,7 @@ Check also in the [Troubleshooting](#troubleshooting) paragrah of the README if
|
||||
Issues not containing the minimum requirements will be closed:
|
||||
|
||||
- Issues without a description (using the header is not good enough) will be closed.
|
||||
- Issues without configuration will be closed
|
||||
- Issues that don't follow this template could be closed
|
||||
|
||||
-->
|
||||
|
||||
|
||||
49
.github/workflows/testus.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
testu:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install -r requirements_test.txt
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
pytest \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
-n auto \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
tests
|
||||
|
||||
- name: Coverage
|
||||
run: |
|
||||
coverage run -m pytest tests/
|
||||
coverage report
|
||||
|
||||
- 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
|
||||
3
.gitignore
vendored
@@ -110,3 +110,6 @@ __pycache__
|
||||
config/**
|
||||
custom_components/hacs
|
||||
custom_components/localtuya
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
6
.vscode/tasks.json
vendored
@@ -13,6 +13,12 @@
|
||||
"command": "./container restart",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Start coverage",
|
||||
"type": "shell",
|
||||
"command": "./container coverage",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Home Assistant translations update",
|
||||
"type": "shell",
|
||||
|
||||
199
README-fr.md
@@ -17,6 +17,7 @@
|
||||
- [HACS installation (recommendé)](#hacs-installation-recommendé)
|
||||
- [Installation manuelle](#installation-manuelle)
|
||||
- [Configuration](#configuration)
|
||||
- [Création d'un nouveau Versatile Thermostat](#création-dun-nouveau-versatile-thermostat)
|
||||
- [Choix des attributs de base](#choix-des-attributs-de-base)
|
||||
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
|
||||
- [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
|
||||
@@ -34,6 +35,12 @@
|
||||
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
|
||||
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
|
||||
- [Configuration avancée](#configuration-avancée)
|
||||
- [Le contrôle centralisé](#le-contrôle-centralisé)
|
||||
- [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale)
|
||||
- [Configuration](#configuration-1)
|
||||
- [Comment trouver le bon service ?](#comment-trouver-le-bon-service-)
|
||||
- [Les évènements](#les-évènements)
|
||||
- [Avertissement](#avertissement)
|
||||
- [Synthèse des paramètres](#synthèse-des-paramètres)
|
||||
- [Exemples de réglage](#exemples-de-réglage)
|
||||
- [Chauffage électrique](#chauffage-électrique)
|
||||
@@ -63,6 +70,9 @@
|
||||
- [Utilisation d'un Heatzy](#utilisation-dun-heatzy)
|
||||
- [Utilisation d'un radiateur avec un fil pilote](#utilisation-dun-radiateur-avec-un-fil-pilote)
|
||||
- [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe)
|
||||
- [Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne](#le-radiateur-chauffe-alors-que-la-température-de-consigne-est-dépassée-ou-ne-chauffe-pas-alors-que-la-température-de-la-pièce-est-bien-en-dessous-de-la-consigne)
|
||||
- [Type `over_switch` ou `over_valve`](#type-over_switch-ou-over_valve)
|
||||
- [Type `over_climate`](#type-over_climate)
|
||||
- [Régler les paramètres de détection d'ouverture de fenêtre en mode auto](#régler-les-paramètres-de-détection-douverture-de-fenêtre-en-mode-auto)
|
||||
- [Pourquoi mon Versatile Thermostat se met en Securite ?](#pourquoi-mon-versatile-thermostat-se-met-en-securite-)
|
||||
- [Comment détecter le mode sécurité ?](#comment-détecter-le-mode-sécurité-)
|
||||
@@ -74,14 +84,18 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
|
||||
|
||||
>  _*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)
|
||||
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
<details>
|
||||
<summary>Autres versions</summary>
|
||||
|
||||
> * **Release 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)
|
||||
<details>
|
||||
<summary>Autres versions</summary>
|
||||
|
||||
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
|
||||
> * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
|
||||
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
|
||||
@@ -114,7 +128,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 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
@@ -137,6 +151,7 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
|
||||
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
|
||||
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 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 ?
|
||||
|
||||
@@ -150,7 +165,9 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
||||
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
|
||||
- La **gestion de la présence à domicile**. Cette fonctionnalité vous permet de modifier dynamiquement la température du préréglage en tenant compte d'un capteur de présence de votre maison.
|
||||
- Des **services pour interagir avec le thermostat** à partir d'autres intégrations : vous pouvez forcer la présence / la non-présence à l'aide d'un service, et vous pouvez modifier dynamiquement la température des préréglages et changer les paramètres de sécurité.
|
||||
- Ajouter des capteurs pour voir les états internes du thermostat.
|
||||
- Ajouter des capteurs pour voir les états internes du thermostat,
|
||||
- Contrôle centralisé de tous les Versatile Thermostat pour les stopper tous, les passer tous en hors-gel, les forcer en mode Chauffage (l'hiver), les forcer en mode Climatisation (l'été).
|
||||
- Contrôle d'une chaudière centrale et des VTherm qui doivent contrôler cette chaudière.
|
||||
|
||||
# Comment installer cet incroyable Thermostat Versatile ?
|
||||
|
||||
@@ -175,8 +192,17 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
||||
|
||||
# Configuration
|
||||
|
||||
Note: aucune configuration dans configuration.yaml n'est nécessaire car toute la configuration est effectuée via l'interface graphique standard lors de l'ajout de l'intégration.
|
||||
-- VTherm = Versatile Thermostat dans la suite de ce document --
|
||||
|
||||
>  _*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.
|
||||
> 2. Certains aspects sont configurés de manière centralisée. Cela permet par ex. définir la température min/max, la détection de fenêtre ouverte,… au niveau d'une instance centrale et unique. Pour chaque VTherm que vous configurez, vous pouvez alors choisir d'utiliser la configuration centrale ou de la remplacer par des paramètres personnalisés.
|
||||
> 3. En plus de cette configuration centralisée, tous les VTherm peuvent être contrôlées par une seule entité de type `select`. Cette fonction est nommé `central_mode`. Cela permet de stopper / démarrer / mettre en hors gel / etc tous les VTherms en une seule fois. Pour chaque VTherm, l'utilisateur indique si il est concerné par ce `central_mode`.
|
||||
|
||||
|
||||
## Création d'un nouveau Versatile Thermostat
|
||||
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
|
||||
|
||||

|
||||
@@ -187,7 +213,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)
|
||||
@@ -197,7 +225,8 @@ Donnez les principaux attributs obligatoires :
|
||||
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait,
|
||||
7. les températures minimales et maximales du thermostat,
|
||||
8. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil,
|
||||
9. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
|
||||
9. la possibilité de controler le thermostat de façon centralisée. Cf [controle centralisé](#le-contrôle-centralisé),
|
||||
10. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
|
||||
|
||||
>  _*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**,
|
||||
@@ -506,15 +535,139 @@ Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque
|
||||
|
||||
Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
|
||||
|
||||
Depuis la version 5.3 il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
|
||||
```
|
||||
versatile_thermostat:
|
||||
...
|
||||
safety_mode:
|
||||
check_outdoor_sensor: false
|
||||
```
|
||||
Par défaut, le thermomètre extérieur peut déclencher une mise en sécurité si il n'envoit plus de valeur.
|
||||
|
||||
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,
|
||||
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
|
||||
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
|
||||
|
||||
## Le contrôle centralisé
|
||||
Depuis la release 5.2, si vous avez défini une configuration centralisée, vous avez une nouvelle entité nommée `select.central_mode` qui permet de piloter tous les VTherms avec une seule action. Pour qu'un VTherm soit contrôlable de façon centralisée, il faut que son attribut de configuration nommé `use_central_mode` soit vrai.
|
||||
|
||||
Cette entité se présente sous la forme d'une liste de choix qui contient les choix suivants :
|
||||
1. `Auto` : le mode 'normal' dans lequel chaque VTherm se comporte comme dans les versions précédentes,
|
||||
2. `Stooped` : tous les VTherms sont mis à l'arrêt (`hvac_off`),
|
||||
3. `Heat only` : tous les VTherms sont mis en mode chauffage lorsque ce mode est supporté par le VTherm, sinon il est stoppé,
|
||||
3. `Cool only` : tous les VTherms sont mis en mode climatisation lorsque ce mode est supporté par le VTherm, sinon il est stoppé,
|
||||
4. `Frost protection` : tous les VTherms sont mis en preset hors-gel lorsque ce preset est supporté par le VTherm, sinon il est stoppé.
|
||||
|
||||
Il est donc possible de contrôler tous les VTherms (que ceux que l'on désigne explicitement) avec un seul contrôle.
|
||||
Exemple de rendu :
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
Le principe mis en place est globalement le suivant :
|
||||
1. une nouvelle entité de type `binary_sensor` et nommée par défaut `binary_sensor.central_boiler` est ajoutée,
|
||||
2. dans la configuration des VTherms vous indiquez si le VTherm doit contrôler la chaudière. En effet, dans une installation hétérogène, certains VTherm doivent commander la chaudière et d'autres non. Vous devez donc indiquer dans chaque configuration de VTherm si il contrôle la chaudière ou pas,
|
||||
3. le `binary_sensor.central_boiler` écoute les changements d'états des équipements des VTherm marqués comme contrôlant la chaudière,
|
||||
4. dès que le nombre d'équipements pilotés par le VTherm demandant du chauffage (ie son `hvac_action` passe à `Heating`) dépasse un seuil paramétrable, alors le `binary_sensor.central_boiler` passe à `on` et **si un service d'activation a été configuré, alors ce service est appelé**,
|
||||
5. si le nombre d'équipements nécessitant du chauffage repasse en dessous du seuil, alors le `binary_sensor.central_boiler` passe à `off` et si **un service de désactivation a été configuré, alors ce service est appelé**,
|
||||
6. vous avez accès à deux entités :
|
||||
- une de type `number` nommé par défaut `number.boiler_activation_threshold`, donne le seuil de déclenchement. Ce seuil est en nombre d'équipements (radiateurs) qui demande du chauffage.
|
||||
- une de type `sensor` nommé par défaut `sensor.nb_device_active_for_boiler`, donne le nombre d'équipements qui demande du chauffage. Par exemple, un VTherm ayant 4 vannes dont 3 demandes du chauffage fera passé ce capteur à 3. Seuls les équipements des VTherms qui sont marqués pour contrôler la chaudière centrale sont comptabilisés.
|
||||
|
||||
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),
|
||||
2. `entity_id` est le nom de l'entité qui commande la chaudière sous la forme `domain.entity_name`. Par exemple: `switch.chaudiere` pour les chaudière commandée par un switch ou `climate.chaudière` pour une chaudière commandée par un thermostat ou tout autre entité qui permet le contrôle de la chaudière (il n'y a pas de limitation). On peut aussi commuter des entrées (`helpers`) comme des `input_boolean` ou `input_number`.
|
||||
3. `service_id` est le nom du service à appeler sous la forme `domain.service_name`. Par exemple: `switch.turn_on`, `switch.turn_off`, `climate.set_temperature`, `climate.set_hvac_mode` sont des exemples valides.
|
||||
4. pour certain service vous aurez besoin d'un paramètre. Cela peut être le 'Mode CVC' `climate.set_hvac_mode` ou la température cible pour `climate.set_temperature`. Ce paramètre doit être configuré sous la forme `attribut:valeur` en fin de chaine.
|
||||
|
||||
Exemples (à ajuster à votre cas) :
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:heat` : pour allumer le thermostat de la chaudière en mode chauffage,
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:off` : pour stopper le thermostat de la chaudière,
|
||||
- `switch.pompe_chaudiere/switch.turn_on` : pour allumer le swicth qui alimente la pompe de la chaudière,
|
||||
- `switch.pompe_chaudiere/switch.turn_off` : pour allumer le swicth qui alimente la pompe de la chaudière,
|
||||
- ...
|
||||
|
||||
### Comment trouver le bon service ?
|
||||
Pour trouver le services a utiliser, le mieux est d'aller dans "Outils de développement / Services", chercher le service a appelé, l'entité à commander et l'éventuel paramètre à donner.
|
||||
Cliquez sur 'Appeler le service'. Si votre chaudière s'allume vous avez la bonne configuration. Passez alors en mode Yaml et recopiez les paramètres.
|
||||
|
||||
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`)
|
||||
|
||||
Faite alors de même pour le service d'extinction et vous êtes parés.
|
||||
|
||||
### Les évènements
|
||||
|
||||
A chaque allumage ou extinction réussie de la chaudière un évènement est envoyé par Versatile Thermostat. Il peut avantageusement être capté par une automatisation, par exemple pour notifier un changement.
|
||||
Les évènements ressemblent à ça :
|
||||
|
||||
Un évènement d'allumage :
|
||||
```
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: true
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:33:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFARW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
Un évènement d'extinction :
|
||||
```
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: false
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:43:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFBRW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
### Avertissement
|
||||
|
||||
>  _*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
|
||||
|
||||
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "configuration centrale" |
|
||||
@@ -527,6 +680,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
|
||||
| ``temp_min`` | Température minimale permise | X | X | X | X |
|
||||
| ``temp_max`` | Température maximale permise | X | X | X | X |
|
||||
| ``device_power`` | Puissance de l'équipement | X | X | X | - |
|
||||
| ``use_central_mode`` | Autorisation du contrôle centralisé | X | X | X | - |
|
||||
| ``use_window_feature`` | Avec détection des ouvertures | X | X | X | - |
|
||||
| ``use_motion_feature`` | Avec détection de mouvement | X | X | X | - |
|
||||
| ``use_power_feature`` | Avec gestion de la puissance | X | X | X | - |
|
||||
@@ -582,7 +736,11 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
|
||||
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
|
||||
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
|
||||
| ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - | - |
|
||||
| ``auto_fan_mode` | Mode de ventilation automatique | - | X | - | - |
|
||||
| ``auto_fan_mode`` | Mode de ventilation automatique | - | 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 |
|
||||
| ``used_by_controls_central_boiler`` | Indique si le VTherm contrôle la chaudière centrale | X | X | X | - |
|
||||
|
||||
# Exemples de réglage
|
||||
|
||||
@@ -778,6 +936,7 @@ Les évènements notifiés sont les suivants:
|
||||
- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes
|
||||
- ``versatile_thermostat_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
|
||||
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
|
||||
- ``versatile_thermostat_central_boiler_event`` : un évènement indiquant un changement dans l'état de la chaudière.
|
||||
|
||||
Si vous avez bien suivi, lorsqu'un thermostat passe en mode sécurité, 3 évènements sont déclenchés :
|
||||
1. ``versatile_thermostat_temperature_event`` pour indiquer qu'un thermomètre ne répond plus,
|
||||
@@ -835,6 +994,9 @@ Les attributs personnalisés sont les suivants :
|
||||
| ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne |
|
||||
| ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation |
|
||||
| ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) |
|
||||
| ``is_controlled_by_central_mode`` | True si le VTherm peut être controlé de façon centrale |
|
||||
| ``last_central_mode`` | Le dernier mode central utilisé (None si le VTherm n'est pas controlé en central) |
|
||||
| ``is_used_by_central_boiler`` | Indique si le VTherm peut contrôler la chaudière centrale |
|
||||
|
||||
# Quelques résultats
|
||||
|
||||
@@ -1008,6 +1170,10 @@ Remplacez les valeurs entre [[ ]] par les votres.
|
||||
step: day
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -1156,6 +1322,19 @@ Exemple :
|
||||
En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation.
|
||||
Cela est tout à fait normal et voulu. C'est décrit ici : [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
|
||||
|
||||
## Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne
|
||||
|
||||
### Type `over_switch` ou `over_valve`
|
||||
Avec un VTherm de type `over_switch` ou `over_valve`, ce défaut montre juste que les paramètres de l'algorithme TPI sont mal réglés. Voir [Algorithme TPI](#algorithme-tpi) pour optimiser les réglages.
|
||||
|
||||
### Type `over_climate`
|
||||
Avec un VTherm de type `over_climate`, la régulation est faite par le `climate` sous-jacent directement et VTherm se contente de lui transmettre les consignes. Donc si le radiateur chauffe alors que la température de consigne est dépassée, c'est certainement que sa mesure de température interne est biaisée. Ca arrive très souvent avec les TRV et les clims réversibles qui ont un capteur de température interne, soit trop près de l'élément de chauffe (donc trop froid l'hiver).
|
||||
|
||||
Exemple de discussion autour de ces sujets: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278)
|
||||
|
||||
Pour s'en sortir, VTherm est équipé d'une fonction nommée auto-régulation qui permet d'adapter la consigne envoyée au sous-jacent jusqu'à ce que la consigne soit respectée. Cette fonction permet de compenser le biais de mesure des thermomètres internes. Si le biais est important la régulation doit être importante. Voir [L'auto-régulation](#lauto-régulation) pour configurer l'auto-régulation.
|
||||
|
||||
|
||||
## Régler les paramètres de détection d'ouverture de fenêtre en mode auto
|
||||
|
||||
Si vous n'arrivez pas à régler la fonction de détection des ouvertures en mode auto (cf. [auto](#le-mode-auto)), vous pouvez essayer de modifier les paramètres de l'algorithme de lissage de la température.
|
||||
|
||||
201
README.md
@@ -17,6 +17,7 @@
|
||||
- [HACS installation (recommended)](#hacs-installation-recommended)
|
||||
- [Manual installation](#manual-installation)
|
||||
- [Configuration](#configuration)
|
||||
- [Creation of a new Versatile Thermostat](#creation-of-a-new-versatile-thermostat)
|
||||
- [Minimal configuration update](#minimal-configuration-update)
|
||||
- [Select the driven entity](#select-the-driven-entity)
|
||||
- [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat)
|
||||
@@ -34,6 +35,12 @@
|
||||
- [Configure the power management](#configure-the-power-management)
|
||||
- [Configure presence or occupancy](#configure-presence-or-occupancy)
|
||||
- [Advanced configuration](#advanced-configuration)
|
||||
- [Centralized control](#centralized-control)
|
||||
- [Control of a central boiler](#control-of-a-central-boiler)
|
||||
- [Setup](#setup)
|
||||
- [How to find the right service?](#how-to-find-the-right-service)
|
||||
- [The events](#the-events)
|
||||
- [Warning](#warning)
|
||||
- [Parameters synthesis](#parameters-synthesis)
|
||||
- [Examples tuning](#examples-tuning)
|
||||
- [Electrical heater](#electrical-heater)
|
||||
@@ -63,6 +70,9 @@
|
||||
- [Using a Heatzy](#using-a-heatzy)
|
||||
- [Using a Heatsink with a Pilot Wire](#using-a-heatsink-with-a-pilot-wire)
|
||||
- [Only the first radiator heats](#only-the-first-radiator-heats)
|
||||
- [The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint](#the-radiator-heats-up-even-though-the-setpoint-temperature-is-exceeded-or-does-not-heat-up-even-though-the-room-temperature-is-well-below-the-setpoint)
|
||||
- [Type `over_switch` or `over_valve`](#type-over_switch-or-over_valve)
|
||||
- [Type `over_climate`](#type-over_climate)
|
||||
- [Adjust window opening detection parameters in auto mode](#adjust-window-opening-detection-parameters-in-auto-mode)
|
||||
- [Why does my Versatile Thermostat go into Safety?](#why-does-my-versatile-thermostat-go-into-safety)
|
||||
- [How to detect safety mode?](#how-to-detect-safety-mode)
|
||||
@@ -74,16 +84,20 @@
|
||||
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*_
|
||||
> * **Release 5.4**: Added a temperature step [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311). Added some regulation thresholdfor `over_valve` VTherm in order to avoid drowing the battery of TRV devices [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338).
|
||||
> * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
|
||||
> * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
<details>
|
||||
<summary>Others releases</summary>
|
||||
|
||||
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
|
||||
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
|
||||
> * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
|
||||
> * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
|
||||
<details>
|
||||
<summary>Others releases</summary>
|
||||
|
||||
> * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
|
||||
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
|
||||
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
|
||||
@@ -114,7 +128,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 for the beers. It's very nice and encourages me to continue!
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco for the beers. It's very nice and encourages me to continue!
|
||||
|
||||
# When to use / not use
|
||||
This thermostat can control 3 types of equipment:
|
||||
@@ -137,6 +151,7 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
|
||||
2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation.
|
||||
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 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 ?
|
||||
|
||||
@@ -150,7 +165,9 @@ This component named __Versatile thermostat__ manage the following use cases :
|
||||
- Add **power shedding management** or regulation to avoid exceeding a defined total power. When max power is exceeded, a hidden 'power' preset is set on the climate entity. When power goes below the max, the previous preset is restored.
|
||||
- Add **home presence management**. This feature allows you to dynamically change the temperature of preset considering a occupancy sensor of your home.
|
||||
- Add **services to interact with the thermostat** from others integration: you can force the presence / un-presence using a service, and you can dynamically change the temperature of the presets and change dynamically the safety parameters.
|
||||
- Add sensors to see the internal states of the thermostat
|
||||
- Add sensors to see the internal states of the thermostat,
|
||||
- Centralized control of all Versatile Thermostats to stop them all, switch them all to frost protection, force them into Heating mode (winter), force them into Cooling mode (summer).
|
||||
- Control of a central boiler and the VTherms which must control this boiler.
|
||||
|
||||
# How to install this incredible Versatile Thermostat ?
|
||||
|
||||
@@ -175,8 +192,17 @@ This component named __Versatile thermostat__ manage the following use cases :
|
||||
|
||||
# Configuration
|
||||
|
||||
Note: no configuration in configuration.yaml is needed because all configuration is done through the standard GUI when adding the integration.
|
||||
-- VTherm = Versatile Thermostat in the remainder of this document --
|
||||
|
||||
>  _*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.
|
||||
> 2. Some aspects are configured centrally. This allows e.g. define the min/max temperature, open window detection, etc. at the level of a single central instance. For each VTherm you configure, you can then choose to use the central configuration or override it with custom settings.
|
||||
> 3. In addition to this centralized configuration, all VTherms can be controlled by a single entity of type `select`. This function is named `central_mode`. This allows you to stop / start / freeze / etc. all VTherms at once. For each VTherm, the user indicates whether he is affected by this `central_mode`.
|
||||
|
||||
|
||||
## Creation of a new Versatile Thermostat
|
||||
Click on Add integration button in the integration page
|
||||

|
||||
|
||||
@@ -185,7 +211,10 @@ The configuration can be change through the same interface. Simply select the th
|
||||
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)
|
||||
@@ -195,7 +224,8 @@ Give the main mandatory attributes:
|
||||
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it,
|
||||
7. minimum and maximum thermostat temperatures,
|
||||
8. the power of the l'équipement which will activate the power and energy sensors of the device,
|
||||
9. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
|
||||
9. the possibility of controlling the thermostat centrally. Cf [centralized control](#centralized-control),
|
||||
10. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
|
||||
|
||||
>  _*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**,
|
||||
@@ -491,6 +521,15 @@ Setting this parameter to ``0.00`` will trigger the safety preset regardless of
|
||||
|
||||
The fourth parameter (``security_default_on_percent``) is the ``on_percent`` value that will be used when the thermostat enters ``safety`` mode. If you put ``0`` then the thermostat will be cut off when it goes into ``safety`` mode, putting 0.2% for example allows you to keep a little heating (20% in this case), even in mode ``safety``. It avoids finding your home totally frozen during a thermometer failure.
|
||||
|
||||
Since version 5.3 it is possible to deactivate the safety device following a lack of data from the outdoor thermometer. Indeed, this most of the time having a low impact on regulation (depending on your settings), it is possible that it is absent without endangering the home. To do this, you must add the following lines to your `configuration.yaml`:
|
||||
```
|
||||
versatile_thermostat:
|
||||
...
|
||||
safety_mode:
|
||||
check_outdoor_sensor: false
|
||||
```
|
||||
By default, the outdoor thermometer can trigger a trip if it no longer sends a value.
|
||||
|
||||
See [example tuning](#examples-tuning) for common tuning examples
|
||||
|
||||
> _*Notes*_
|
||||
@@ -500,6 +539,121 @@ See [example tuning](#examples-tuning) for common tuning examples
|
||||
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
|
||||
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature.
|
||||
|
||||
## Centralized control
|
||||
Since release 5.2, if you have defined a centralized configuration, you have a new entity named `select.central_mode` which allows you to control all VTherms with a single action. For a VTherm to be centrally controllable, its configuration attribute named `use_central_mode` must be true.
|
||||
|
||||
This entity is presented in the form of a list of choices which contains the following choices:
|
||||
1. `Auto`: the 'normal' mode in which each VTherm behaves as in previous versions,
|
||||
2. `Stooped`: all VTherms are turned off (`hvac_off`),
|
||||
3. `Heat only`: all VTherms are put in heating mode when this mode is supported by the VTherm, otherwise it is stopped,
|
||||
3. `Cool only`: all VTherms are put in cooling mode when this mode is supported by the VTherm, otherwise it is stopped,
|
||||
4. `Frost protection`: all VTherms are put in frost protection preset when this preset is supported by the VTherm, otherwise it is stopped.
|
||||
|
||||
It is therefore possible to control all VTherms (only those explicitly designated) with a single control.
|
||||
Example rendering:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
The principle put in place is generally as follows:
|
||||
1. a new entity of type `binary_sensor` and named by default `binary_sensor.central_boiler` is added,
|
||||
2. in the VTherms configuration you indicate whether the VTherm should control the boiler. Indeed, in a heterogeneous installation, some VTherm must control the boiler and others not. You must therefore indicate in each VTherm configuration whether it controls the boiler or not,
|
||||
3. the `binary_sensor.central_boiler` listens for changes in state of VTherm equipment marked as controlling the boiler,
|
||||
4. as soon as the number of devices controlled by the VTherm requesting heating (ie its `hvac_action` goes to `Heating`) exceeds a configurable threshold, then the `binary_sensor.central_boiler` goes to `on` and **if a activation service has been configured, then this service is called**,
|
||||
5. if the number of devices requiring heating falls below the threshold again, then the `binary_sensor.central_boiler` goes to `off` and if **a deactivation service has been configured, then this service is called**,
|
||||
6. you have access to two entities:
|
||||
- one of type `number` named by default `number.boiler_activation_threshold`, gives the trigger threshold. This threshold is in number of equipment (radiators) which requires heating.
|
||||
- one of type `sensor` named by default `sensor.nb_device_active_for_boiler`, gives the number of devices requiring heating. For example, a VTherm having 4 valves including 3 heating requests will increase this sensor to 3. Only VTherm equipment that is marked to control the central boiler is counted.
|
||||
|
||||
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),
|
||||
2. `entity_id` is the name of the entity that controls the boiler in the form `domain.entity_name`. For example: `switch.boiler` for boilers controlled by a switch or `climate.boiler` for a boiler controlled by a thermostat or any other entity which allows control of the boiler (there is no limitation). We can also switch inputs (`helpers`) like `input_boolean` or `input_number`.
|
||||
3. `service_id` is the name of the service to call in the form `domain.service_name`. For example: `switch.turn_on`, `switch.turn_off`, `climate.set_temperature`, `climate.set_hvac_mode` are valid examples.
|
||||
4. For some service you will need a parameter. This can be the 'HVAC Mode' `climate.set_hvac_mode` or the target temperature for `climate.set_temperature`. This parameter must be configured in the form `attribute:value` at the end of the string.
|
||||
|
||||
Examples (to be adjusted to your case):
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:heat`: to turn on the boiler thermostat in heating mode,
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:off`: to stop the boiler thermostat,
|
||||
- `switch.pompe_chaudiere/switch.turn_on`: to turn on the switch which powers the boiler pump,
|
||||
- `switch.pompe_chaudiere/switch.turn_off`: to turn on the switch which powers the boiler pump,
|
||||
- ...
|
||||
|
||||
### How to find the right service?
|
||||
To find the service to use, the best is to go to "Development tools / Services", look for the service called, the entity to order and the possible parameter to give.
|
||||
Click on 'Call Service'. If your boiler lights up you have the correct configuration. Then switch to Yaml mode and copy the parameters.
|
||||
|
||||
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`)
|
||||
|
||||
Then do the same for the extinguishing service and you are all set.
|
||||
|
||||
### The events
|
||||
|
||||
Each time the boiler is successfully switched on or off, an event is sent by Versatile Thermostat. It can advantageously be captured by automation, for example to notify a change.
|
||||
The events look like this:
|
||||
|
||||
An ignition event:
|
||||
```
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: true
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:33:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFARW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
An extinction event:
|
||||
```
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: false
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:43:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFBRW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
### Warning
|
||||
|
||||
>  _*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
|
||||
|
||||
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "central configuration" |
|
||||
@@ -512,6 +666,7 @@ See [example tuning](#examples-tuning) for common tuning examples
|
||||
| ``temp_min`` | Minimal temperature allowed | X | X | X | X |
|
||||
| ``temp_max`` | Maximal temperature allowed | X | X | X | X |
|
||||
| ``device_power`` | Total device power | X | X | X | - |
|
||||
| ``use_central_mode`` | Allow the centralized control | X | X | X | - |
|
||||
| ``use_window_feature`` | Use window detection | X | X | X | - |
|
||||
| ``use_motion_feature`` | Use motion detection | X | X | X | - |
|
||||
| ``use_power_feature`` | Use power management | X | X | X | - |
|
||||
@@ -568,7 +723,12 @@ See [example tuning](#examples-tuning) for common tuning examples
|
||||
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
|
||||
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
|
||||
| ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - | - |
|
||||
| ``auto_fan_mode` | Auto fan mode | - | X | - | - |
|
||||
| ``auto_fan_mode`` | Auto fan mode | - | 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 |
|
||||
| ``used_by_controls_central_boiler`` | Indicate if the VTherm control the central boiler | X | X | X | - |
|
||||
|
||||
|
||||
# Examples tuning
|
||||
|
||||
@@ -762,6 +922,7 @@ The notified events are as follows:
|
||||
- ``versatile_thermostat_temperature_event``: one or both temperature measurements of a thermostat have not been updated for more than ``security_delay_min`` minutes
|
||||
- ``versatile_thermostat_hvac_mode_event``: the thermostat is on or off. This event is also broadcast when the thermostat starts up
|
||||
- ``versatile_thermostat_preset_event``: a new preset is selected on the thermostat. This event is also broadcast when the thermostat starts up
|
||||
- ``versatile_thermostat_central_boiler_event``: an event indicating a change in the state of the central boiler.
|
||||
|
||||
If you have followed correctly, when a thermostat goes into safety mode, 3 events are triggered:
|
||||
1. ``versatile_thermostat_temperature_event`` to indicate that a thermometer has become unresponsive,
|
||||
@@ -819,6 +980,9 @@ Custom attributes are the following:
|
||||
| ``valve_open_percent`` | The opening percentage of the valve |
|
||||
| ``regulated_target_temperature`` | The self-regulated target temperature calculated |
|
||||
| ``is_inversed`` | True if the command is inversed (pilot wire with diode) |
|
||||
| ``is_controlled_by_central_mode`` | True if the VTherm can be centrally controlled |
|
||||
| ``last_central_mode`` | The last central mode used (None if the VTherm is not centrally controlled) |
|
||||
| ``is_used_by_central_boiler`` | Indicate if the VTherm can control the central boiler |
|
||||
|
||||
# Some results
|
||||
|
||||
@@ -991,6 +1155,11 @@ Replace values in [[ ]] by yours.
|
||||
step: day
|
||||
```
|
||||
|
||||
Example of graph obtained with Plotly :
|
||||
|
||||

|
||||
|
||||
|
||||
## And always better and better with the NOTIFIER daemon app to notify events
|
||||
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
|
||||
|
||||
@@ -1136,7 +1305,19 @@ Example :
|
||||
|
||||
## Only the first radiator heats
|
||||
In `over_switch` mode if several radiators are configured for the same VTherm, switching on will be done sequentially to smooth out consumption peaks as much as possible.
|
||||
This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch)
|
||||
This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch)v
|
||||
|
||||
## The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint
|
||||
|
||||
### Type `over_switch` or `over_valve`
|
||||
With a VTherm of type `over_switch` or `over_valve`, this fault just shows that the parameters of the TPI algorithm are incorrectly set. See [TPI Algorithm](#tpi-algorithm) to optimize the settings.
|
||||
|
||||
### Type `over_climate`
|
||||
With an `over_climate` type VTherm, the regulation is done by the underlying `climate` directly and VTherm simply transmits the instructions to it. So if the radiator heats up when the set temperature is exceeded, it is certainly because its internal temperature measurement is biased. This happens very often with TRVs and reversible air conditioning units which have an internal temperature sensor, or too close to the heating element (therefore too cold in winter).
|
||||
|
||||
Example of discussion around these topics: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312 ), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278)
|
||||
|
||||
To get around this, VTherm is equipped with a function called self-regulation which allows the instruction sent to the underlying to be adapted until the target temperature is respected. This function compensates for the measurement bias of internal thermometers. If the bias is important the regulation must be important. See [Self-regulation](#self-regulation) to configure self-regulation.
|
||||
|
||||
## Adjust window opening detection parameters in auto mode
|
||||
|
||||
|
||||
@@ -43,4 +43,13 @@ case $1 in
|
||||
pwd
|
||||
./scripts/starts_ha.sh
|
||||
;;
|
||||
coverage)
|
||||
rm -rf htmlcov/*
|
||||
echo "Starting coverage tests"
|
||||
coverage run -m pytest tests/
|
||||
echo "Starting coverage report"
|
||||
coverage report
|
||||
echo "Starting coverage html"
|
||||
coverage html
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -24,6 +24,7 @@ from .const import (
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_SHORT_EMA_PARAMS,
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
)
|
||||
@@ -47,12 +48,17 @@ EMA_PARAM_SCHEMA = {
|
||||
vol.Required("precision"): cv.positive_int,
|
||||
}
|
||||
|
||||
SAFETY_MODE_PARAM_SCHEMA = {
|
||||
vol.Required("check_outdoor_sensor"): bool,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
|
||||
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
|
||||
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
|
||||
}
|
||||
),
|
||||
},
|
||||
@@ -105,6 +111,9 @@ async def reload_all_vtherm(hass):
|
||||
]
|
||||
|
||||
await asyncio.gather(*reload_tasks)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -124,6 +133,8 @@ 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()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -133,6 +144,10 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
await reload_all_vtherm(hass)
|
||||
else:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
# Reload the central boiler list of entities
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api is not None:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -142,6 +157,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
if api:
|
||||
api.remove_entry(entry)
|
||||
await api.reload_central_boiler_entities_list()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -116,6 +120,12 @@ from .const import (
|
||||
ATTR_TOTAL_ENERGY,
|
||||
PRESET_AC_SUFFIX,
|
||||
DEFAULT_SHORT_EMA_PARAMS,
|
||||
CENTRAL_MODE_AUTO,
|
||||
CENTRAL_MODE_STOPPED,
|
||||
CENTRAL_MODE_HEAT_ONLY,
|
||||
CENTRAL_MODE_COOL_ONLY,
|
||||
CENTRAL_MODE_FROST_PROTECTION,
|
||||
send_vtherm_event,
|
||||
)
|
||||
|
||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
@@ -128,6 +138,7 @@ from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from .ema import ExponentialMovingAverage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
@@ -139,25 +150,13 @@ 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(
|
||||
{
|
||||
"is_on",
|
||||
"is_controlled_by_central_mode",
|
||||
"last_central_mode",
|
||||
"type",
|
||||
"frost_temp",
|
||||
"eco_temp",
|
||||
@@ -189,6 +188,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"window_action",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
@@ -196,12 +196,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
"target_temperature_step",
|
||||
"is_used_by_central_boiler",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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__()
|
||||
@@ -259,23 +266,30 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._window_auto_state = False
|
||||
self._window_auto_on = False
|
||||
self._window_auto_algo = None
|
||||
# PR - Adding Window ByPass
|
||||
self._window_bypass_state = False
|
||||
self._window_action = None
|
||||
|
||||
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
self._last_change_time = None
|
||||
|
||||
self._underlyings = []
|
||||
self._underlyings: list[UnderlyingEntity] = []
|
||||
|
||||
self._ema_temp = None
|
||||
self._ema_algo = None
|
||||
self._now = None
|
||||
|
||||
self._attr_fan_mode = None
|
||||
|
||||
self._is_central_mode = None
|
||||
self._last_central_mode = None
|
||||
self._is_used_by_central_boiler = 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):
|
||||
@@ -321,7 +335,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(
|
||||
@@ -340,9 +354,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
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 = {}
|
||||
presets: dict[str, Any] = {}
|
||||
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
|
||||
for key, value in items:
|
||||
_LOGGER.debug("looking for key=%s, value=%s", key, value)
|
||||
@@ -354,7 +370,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._attr_max_temp if self._ac_mode else self._attr_min_temp
|
||||
)
|
||||
|
||||
presets_away = {}
|
||||
presets_away: dict[str, Any] = {}
|
||||
items = (
|
||||
CONF_PRESETS_AWAY_WITH_AC.items()
|
||||
if self._ac_mode
|
||||
@@ -385,8 +401,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)
|
||||
@@ -400,7 +414,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
|
||||
self._window_auto_on = (
|
||||
self._window_auto_open_threshold is not None
|
||||
self._window_sensor_entity_id is None
|
||||
and self._window_auto_open_threshold is not None
|
||||
and self._window_auto_open_threshold > 0.0
|
||||
and self._window_auto_close_threshold is not None
|
||||
and self._window_auto_max_duration is not None
|
||||
@@ -434,6 +449,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._presence_on = self._presence_sensor_entity_id is not None
|
||||
|
||||
if self._ac_mode:
|
||||
# Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
|
||||
# Some over_switch can do both heating and cooling
|
||||
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
||||
else:
|
||||
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
||||
@@ -459,7 +476,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._saved_preset_mode = PRESET_NONE
|
||||
|
||||
# Power management
|
||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER)
|
||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
|
||||
self._pmax_on = False
|
||||
self._current_power = None
|
||||
self._current_power_max = None
|
||||
@@ -552,6 +569,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
short_ema_params.get("max_alpha"),
|
||||
)
|
||||
|
||||
self._is_central_mode = not (
|
||||
entry_infos.get(CONF_USE_CENTRAL_MODE) is False
|
||||
) # Default value (None) is True
|
||||
|
||||
self._is_used_by_central_boiler = (
|
||||
entry_infos.get(CONF_USED_BY_CENTRAL_BOILER) is True
|
||||
)
|
||||
|
||||
self._window_action = (
|
||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||
self,
|
||||
@@ -789,7 +818,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
|
||||
@@ -870,7 +899,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._hvac_mode,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@property
|
||||
@@ -900,19 +929,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
|
||||
|
||||
@@ -994,17 +1023,23 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
return action
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
def is_used_by_central_boiler(self) -> HVACAction | None:
|
||||
"""Return true is the VTherm is configured to be used by
|
||||
central boiler"""
|
||||
return self._is_used_by_central_boiler
|
||||
|
||||
@property
|
||||
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:
|
||||
@@ -1012,7 +1047,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
|
||||
|
||||
@@ -1062,6 +1097,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get the Window Bypass"""
|
||||
return self._window_bypass_state
|
||||
|
||||
@property
|
||||
def window_action(self) -> bool | None:
|
||||
"""Get the Window Action"""
|
||||
return self._window_action
|
||||
|
||||
@property
|
||||
def security_state(self) -> bool | None:
|
||||
"""Get the security_state"""
|
||||
@@ -1125,11 +1165,27 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Returns the number of underlying entities"""
|
||||
return len(self._underlyings)
|
||||
|
||||
@property
|
||||
def underlying_entities(self) -> int:
|
||||
"""Returns the underlying entities"""
|
||||
return self._underlyings
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""True if the VTherm is on (! HVAC_OFF)"""
|
||||
return self.hvac_mode and self.hvac_mode != HVACMode.OFF
|
||||
|
||||
@property
|
||||
def is_controlled_by_central_mode(self) -> bool:
|
||||
"""Returns True if this VTherm can be controlled by the central_mode"""
|
||||
return self._is_central_mode
|
||||
|
||||
@property
|
||||
def last_central_mode(self) -> str | None:
|
||||
"""Returns the last central_mode taken into account.
|
||||
Is None if the VTherm is not controlled by central_mode"""
|
||||
return self._last_central_mode
|
||||
|
||||
def underlying_entity_id(self, index=0) -> str | None:
|
||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||
if index < self.nb_underlying_entities:
|
||||
@@ -1160,7 +1216,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Turn auxiliary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True):
|
||||
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)
|
||||
|
||||
@@ -1177,11 +1233,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
|
||||
if self._ac_mode:
|
||||
if self._hvac_mode == HVACMode.COOL:
|
||||
if self.preset_mode != PRESET_FROST_PROTECTION:
|
||||
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
|
||||
else:
|
||||
await self._async_set_preset_mode_internal(PRESET_ECO, True)
|
||||
await self._async_set_preset_mode_internal(PRESET_ECO, True, False)
|
||||
|
||||
if need_control_heating and sub_need_control_heating:
|
||||
await self.async_control_heating(force=True)
|
||||
@@ -1195,12 +1251,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.async_write_ha_state()
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
@overrides
|
||||
async def async_set_preset_mode(
|
||||
self, preset_mode: str, overwrite_saved_preset=True
|
||||
):
|
||||
"""Set new preset mode."""
|
||||
await self._async_set_preset_mode_internal(preset_mode)
|
||||
await self._async_set_preset_mode_internal(
|
||||
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
|
||||
)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_set_preset_mode_internal(self, preset_mode, force=False):
|
||||
async def _async_set_preset_mode_internal(
|
||||
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)
|
||||
if (
|
||||
@@ -1215,10 +1278,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
# I don't think we need to call async_write_ha_state if we didn't change the state
|
||||
return
|
||||
|
||||
# In security mode don't change preset but memorise the new expected preset when security will be off
|
||||
# In safety mode don't change preset but memorise the new expected preset when security will be off
|
||||
if preset_mode != PRESET_SECURITY and self._security_state:
|
||||
_LOGGER.debug(
|
||||
"%s - is in security mode. Just memorise the new expected ", self
|
||||
"%s - is in safety mode. Just memorise the new expected ", self
|
||||
)
|
||||
if preset_mode not in HIDDEN_PRESETS:
|
||||
self._saved_preset_mode = preset_mode
|
||||
@@ -1242,18 +1305,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self.reset_last_temperature_time(old_preset_mode)
|
||||
|
||||
self.save_preset_mode()
|
||||
if overwrite_saved_preset:
|
||||
self.save_preset_mode()
|
||||
self.recalculate()
|
||||
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
|
||||
@@ -1263,7 +1327,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
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 (
|
||||
@@ -1299,11 +1363,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
else:
|
||||
return self._presets_away[self.get_preset_away_name(preset_mode)]
|
||||
|
||||
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
|
||||
@@ -1313,7 +1377,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
|
||||
@@ -1330,14 +1394,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)
|
||||
@@ -1345,7 +1409,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)
|
||||
@@ -1372,9 +1436,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return
|
||||
|
||||
await self._async_update_temp(new_state)
|
||||
dearm_window_auto = await self._async_update_temp(new_state)
|
||||
self.recalculate()
|
||||
await self.async_control_heating(force=False)
|
||||
return dearm_window_auto
|
||||
|
||||
async def _async_ext_temperature_changed(self, event: Event):
|
||||
"""Handle external temperature opf the sensor changes."""
|
||||
@@ -1433,26 +1498,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._window_state = new_state.state == STATE_ON
|
||||
|
||||
# PR - Adding Window ByPass
|
||||
_LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
|
||||
if self._window_bypass_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window ByPass is activated. Ignore window event", self
|
||||
)
|
||||
else:
|
||||
if not self._window_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s'",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
)
|
||||
await self.restore_hvac_mode(True)
|
||||
elif self._window_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
|
||||
)
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
await self.change_window_detection_state(self._window_state)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
if new_state is None or old_state is None or new_state.state == old_state.state:
|
||||
@@ -1581,6 +1634,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
for under in self._underlyings:
|
||||
await under.check_initial_state(self._hvac_mode)
|
||||
|
||||
# Starts the initial control loop (don't wait for an update of temperature)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
@callback
|
||||
async def _async_update_temp(self, state: State):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
@@ -1604,12 +1660,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
state.last_changed.astimezone(self._current_tz),
|
||||
)
|
||||
|
||||
# try to restart if we were in security mode
|
||||
# try to restart if we were in safety mode
|
||||
if self._security_state:
|
||||
await self.check_security()
|
||||
await self.check_safety()
|
||||
|
||||
# check window_auto
|
||||
await self._async_manage_window_auto()
|
||||
return await self._async_manage_window_auto()
|
||||
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
|
||||
@@ -1631,14 +1687,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
state.last_changed.astimezone(self._current_tz),
|
||||
)
|
||||
|
||||
# try to restart if we were in security mode
|
||||
# try to restart if we were in safety mode
|
||||
if self._security_state:
|
||||
await self.check_security()
|
||||
await self.check_safety()
|
||||
except ValueError as ex:
|
||||
_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)
|
||||
@@ -1664,7 +1720,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)
|
||||
@@ -1689,7 +1747,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(
|
||||
@@ -1705,7 +1765,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
|
||||
@@ -1790,7 +1850,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = False
|
||||
await self.restore_hvac_mode(True)
|
||||
await self.change_window_detection_state(self._window_auto_state)
|
||||
# await self.restore_hvac_mode(True)
|
||||
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
@@ -1816,8 +1877,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
slope if slope is not None else 0.0,
|
||||
)
|
||||
|
||||
if self.window_bypass_state:
|
||||
_LOGGER.info("%s - Window auto event is ignored because bypass is ON", self)
|
||||
if self.window_bypass_state or not self.is_window_auto_enabled:
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
|
||||
self,
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
@@ -1847,8 +1911,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = True
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
await self.change_window_detection_state(self._window_auto_state)
|
||||
# self.save_hvac_mode()
|
||||
# await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
# Arm the end trigger
|
||||
if self._window_call_cancel:
|
||||
@@ -1998,6 +2063,69 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
return self._overpowering_state
|
||||
|
||||
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
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - Central mode have change from %s to %s",
|
||||
self,
|
||||
old_central_mode,
|
||||
new_central_mode,
|
||||
)
|
||||
|
||||
self._last_central_mode = new_central_mode
|
||||
|
||||
def save_all():
|
||||
"""save preset and hvac_mode"""
|
||||
self.save_preset_mode()
|
||||
self.save_hvac_mode()
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_AUTO:
|
||||
if self.window_state is not STATE_ON:
|
||||
await self.restore_hvac_mode()
|
||||
await self.restore_preset_mode()
|
||||
|
||||
return
|
||||
|
||||
if old_central_mode == CENTRAL_MODE_AUTO and self.window_state is not STATE_ON:
|
||||
save_all()
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_STOPPED:
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
|
||||
if HVACMode.COOL in self.hvac_modes:
|
||||
await self.async_set_hvac_mode(HVACMode.COOL)
|
||||
else:
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
|
||||
if HVACMode.HEAT in self.hvac_modes:
|
||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||
else:
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_FROST_PROTECTION:
|
||||
if (
|
||||
PRESET_FROST_PROTECTION in self.preset_modes
|
||||
and HVACMode.HEAT in self.hvac_modes
|
||||
):
|
||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await self.async_set_preset_mode(
|
||||
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
|
||||
)
|
||||
else:
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
def _set_now(self, now: datetime):
|
||||
"""Set the now timestamp. This is only for tests purpose"""
|
||||
self._now = now
|
||||
@@ -2007,7 +2135,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||
return self._now if self._now is not None else datetime.now(self._current_tz)
|
||||
|
||||
async def check_security(self) -> bool:
|
||||
async def check_safety(self) -> bool:
|
||||
"""Check if last temperature date is too long"""
|
||||
now = self.now
|
||||
delta_temp = (
|
||||
@@ -2019,9 +2147,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
mode_cond = self._hvac_mode != HVACMode.OFF
|
||||
|
||||
temp_cond: bool = (
|
||||
delta_temp > self._security_delay_min
|
||||
or delta_ext_temp > self._security_delay_min
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
is_outdoor_checked = (
|
||||
not api.safety_mode
|
||||
or api.safety_mode.get("check_outdoor_sensor") is not False
|
||||
)
|
||||
|
||||
temp_cond: bool = delta_temp > self._security_delay_min or (
|
||||
is_outdoor_checked and delta_ext_temp > self._security_delay_min
|
||||
)
|
||||
climate_cond: bool = self.is_over_climate and self.hvac_action not in [
|
||||
HVACAction.COOLING,
|
||||
@@ -2064,7 +2197,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if shouldStartSecurity:
|
||||
if shouldClimateBeInSecurity:
|
||||
_LOGGER.warning(
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode",
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Setting it into safety mode",
|
||||
self,
|
||||
self._security_delay_min,
|
||||
delta_temp,
|
||||
@@ -2073,13 +2206,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
elif shouldSwitchBeInSecurity:
|
||||
_LOGGER.warning(
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode",
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f %%) is over defined value (%.2f %%). Set it into safety mode",
|
||||
self,
|
||||
self._security_delay_min,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
self._prop_algorithm.on_percent,
|
||||
self._security_min_on_percent,
|
||||
self._prop_algorithm.on_percent * 100,
|
||||
self._security_min_on_percent * 100,
|
||||
)
|
||||
|
||||
self.send_event(
|
||||
@@ -2097,7 +2230,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
},
|
||||
)
|
||||
|
||||
# Start security mode
|
||||
# Start safety mode
|
||||
if shouldStartSecurity:
|
||||
self._security_state = True
|
||||
self.save_hvac_mode()
|
||||
@@ -2125,10 +2258,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
},
|
||||
)
|
||||
|
||||
# Stop security mode
|
||||
# Stop safety mode
|
||||
if shouldStopSecurity:
|
||||
_LOGGER.warning(
|
||||
"%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s",
|
||||
"%s - End of safety mode. restoring hvac_mode to %s and preset_mode to %s",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
self._saved_preset_mode,
|
||||
@@ -2165,7 +2298,64 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
should have found the underlying climate to be operational"""
|
||||
return True
|
||||
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
async def change_window_detection_state(self, new_state):
|
||||
"""Change the window detection state.
|
||||
new_state is on if an open window have been detected or off else
|
||||
"""
|
||||
if not new_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if central_mode is not STOPPED",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
)
|
||||
if self._window_action in [CONF_WINDOW_FROST_TEMP, CONF_WINDOW_ECO_TEMP]:
|
||||
await self._async_internal_set_temperature(self._saved_target_temp)
|
||||
# default to TURN_OFF
|
||||
elif self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
|
||||
if self.last_central_mode != CENTRAL_MODE_STOPPED:
|
||||
await self.restore_hvac_mode(True)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - undefined window_action %s. Please open a bug in the github of this project with this log",
|
||||
self,
|
||||
self._window_action,
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
|
||||
)
|
||||
if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
|
||||
if self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
|
||||
self.save_hvac_mode()
|
||||
elif self._window_action in [
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]:
|
||||
self._saved_target_temp = self._target_temp
|
||||
|
||||
if (
|
||||
self._window_action == CONF_WINDOW_FAN_ONLY
|
||||
and HVACMode.FAN_ONLY in self.hvac_modes
|
||||
):
|
||||
await self.async_set_hvac_mode(HVACMode.FAN_ONLY)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_FROST_TEMP
|
||||
and self._presets.get(PRESET_FROST_PROTECTION) is not None
|
||||
):
|
||||
await self._async_internal_set_temperature(
|
||||
self.find_preset_temp(PRESET_FROST_PROTECTION)
|
||||
)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_ECO_TEMP
|
||||
and self._presets.get(PRESET_ECO) is not None
|
||||
):
|
||||
await self._async_internal_set_temperature(
|
||||
self.find_preset_temp(PRESET_ECO)
|
||||
)
|
||||
else: # default is to turn_off
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -2192,7 +2382,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug("%s - End of cycle (overpowering)", self)
|
||||
return True
|
||||
|
||||
security: bool = await self.check_security()
|
||||
security: bool = await self.check_safety()
|
||||
if security and self.is_over_climate:
|
||||
_LOGGER.debug("%s - End of cycle (security and over climate)", self)
|
||||
return True
|
||||
@@ -2233,12 +2423,14 @@ 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,
|
||||
"preset_mode": self.preset_mode,
|
||||
"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],
|
||||
@@ -2256,8 +2448,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.get_preset_away_name(PRESET_COMFORT)
|
||||
),
|
||||
"power_temp": self._power_temp,
|
||||
# Already in super class - "target_temp": self.target_temperature,
|
||||
# Already in super class - "current_temp": self._cur_temp,
|
||||
"target_temperature_step": self.target_temperature_step,
|
||||
"ext_current_temperature": self._cur_ext_temp,
|
||||
"ac_mode": self._ac_mode,
|
||||
@@ -2266,12 +2456,23 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"saved_preset_mode": self._saved_preset_mode,
|
||||
"saved_target_temp": self._saved_target_temp,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"window_state": self.window_state,
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"motion_state": self._motion_state,
|
||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"overpowering_state": self.overpowering_state,
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"presence_state": self._presence_state,
|
||||
"window_state": self.window_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
"window_bypass_state": self._window_bypass_state,
|
||||
"window_sensor_entity_id": self._window_sensor_entity_id,
|
||||
"window_delay_sec": self._window_delay_sec,
|
||||
"window_auto_enabled": self.is_window_auto_enabled,
|
||||
"window_auto_open_threshold": self._window_auto_open_threshold,
|
||||
"window_auto_close_threshold": self._window_auto_close_threshold,
|
||||
"window_auto_max_duration": self._window_auto_max_duration,
|
||||
"window_action": self.window_action,
|
||||
"security_delay_min": self._security_delay_min,
|
||||
"security_min_on_percent": self._security_min_on_percent,
|
||||
"security_default_on_percent": self._security_default_on_percent,
|
||||
@@ -2290,19 +2491,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
.astimezone(self._current_tz)
|
||||
.isoformat(),
|
||||
"timezone": str(self._current_tz),
|
||||
"window_sensor_entity_id": self._window_sensor_entity_id,
|
||||
"window_delay_sec": self._window_delay_sec,
|
||||
"window_auto_enabled": self.is_window_auto_enabled,
|
||||
"window_auto_open_threshold": self._window_auto_open_threshold,
|
||||
"window_auto_close_threshold": self._window_auto_close_threshold,
|
||||
"window_auto_max_duration": self._window_auto_max_duration,
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"temperature_unit": self.temperature_unit,
|
||||
"is_device_active": self.is_device_active,
|
||||
"ema_temp": self._ema_temp,
|
||||
"is_used_by_central_boiler": self.is_used_by_central_boiler,
|
||||
}
|
||||
|
||||
@callback
|
||||
@@ -2312,7 +2504,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:
|
||||
@@ -2325,7 +2517,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
|
||||
@@ -2363,7 +2558,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:
|
||||
@@ -2374,11 +2574,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
entity_id: climate.thermostat_2
|
||||
"""
|
||||
_LOGGER.info(
|
||||
"%s - Calling service_set_security, delay_min: %s, min_on_percent: %s, default_on_percent: %s",
|
||||
"%s - Calling service_set_security, delay_min: %s, min_on_percent: %s %%, default_on_percent: %s %%",
|
||||
self,
|
||||
delay_min,
|
||||
min_on_percent,
|
||||
default_on_percent,
|
||||
min_on_percent * 100,
|
||||
default_on_percent * 100,
|
||||
)
|
||||
if delay_min:
|
||||
self._security_delay_min = delay_min
|
||||
@@ -2393,7 +2593,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:
|
||||
@@ -2425,8 +2625,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
def send_event(self, event_type: EventType, data: dict):
|
||||
"""Send an event"""
|
||||
_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)
|
||||
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)
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
""" Implements the VersatileThermostat binary sensors component """
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
CoreState,
|
||||
HomeAssistantError,
|
||||
)
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
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
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
@@ -13,8 +24,14 @@ from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import (
|
||||
VersatileThermostatBaseEntity,
|
||||
check_and_extract_service_configuration,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
@@ -22,6 +39,11 @@ from .const import (
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
overrides,
|
||||
EventType,
|
||||
send_vtherm_event,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -42,20 +64,22 @@ async def async_setup_entry(
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
|
||||
entities = [
|
||||
SecurityBinarySensor(hass, unique_id, name, entry.data),
|
||||
WindowByPassBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_USE_MOTION_FEATURE):
|
||||
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_WINDOW_FEATURE):
|
||||
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
|
||||
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_POWER_FEATURE):
|
||||
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
|
||||
entities = [
|
||||
CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
else:
|
||||
entities = [
|
||||
SecurityBinarySensor(hass, unique_id, name, entry.data),
|
||||
WindowByPassBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_USE_MOTION_FEATURE):
|
||||
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_WINDOW_FEATURE):
|
||||
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
|
||||
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_POWER_FEATURE):
|
||||
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
@@ -269,7 +293,6 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
return "mdi:nature-people"
|
||||
|
||||
|
||||
# PR - Adding Window ByPass
|
||||
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the Window ByPass state"""
|
||||
|
||||
@@ -307,3 +330,161 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
return "mdi:window-shutter-cog"
|
||||
else:
|
||||
return "mdi:window-shutter-auto"
|
||||
|
||||
|
||||
class CentralBoilerBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the Central Boiler state"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the CentralBoiler Binary sensor"""
|
||||
self._config_id = unique_id
|
||||
self._attr_name = "Central boiler"
|
||||
self._attr_unique_id = "central_boiler_state"
|
||||
self._attr_is_on = False
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._entities = []
|
||||
self._hass = hass
|
||||
self._service_activate = check_and_extract_service_configuration(
|
||||
entry_infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)
|
||||
)
|
||||
self._service_deactivate = check_and_extract_service_configuration(
|
||||
entry_infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
return BinarySensorDeviceClass.RUNNING
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if self._attr_is_on:
|
||||
return "mdi:water-boiler"
|
||||
else:
|
||||
return "mdi:water-boiler-off"
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
async def listen_nb_active_vtherm_entity(self):
|
||||
"""Initialize the listening of state change of VTherms"""
|
||||
|
||||
# Listen to all VTherm state change
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
|
||||
if (
|
||||
api.nb_active_device_for_boiler_entity
|
||||
and api.nb_active_device_for_boiler_threshold_entity
|
||||
):
|
||||
listener_cancel = async_track_state_change_event(
|
||||
self._hass,
|
||||
[
|
||||
api.nb_active_device_for_boiler_entity.entity_id,
|
||||
api.nb_active_device_for_boiler_threshold_entity.entity_id,
|
||||
],
|
||||
self.calculate_central_boiler_state,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"%s - entity to get the nb of active VTherm is %s",
|
||||
self,
|
||||
api.nb_active_device_for_boiler_entity.entity_id,
|
||||
)
|
||||
self.async_on_remove(listener_cancel)
|
||||
else:
|
||||
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
|
||||
|
||||
await self.calculate_central_boiler_state(None)
|
||||
|
||||
async def calculate_central_boiler_state(self, _):
|
||||
"""Calculate the central boiler state depending on all VTherm that
|
||||
controls this central boiler"""
|
||||
|
||||
_LOGGER.debug("%s - calculating the new central boiler state", self)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
if (
|
||||
api.nb_active_device_for_boiler is None
|
||||
or api.nb_active_device_for_boiler_threshold is None
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"%s - the entities to calculate the boiler state are not initialized. Boiler state cannot be calculated",
|
||||
self,
|
||||
)
|
||||
return False
|
||||
|
||||
active = (
|
||||
api.nb_active_device_for_boiler >= api.nb_active_device_for_boiler_threshold
|
||||
)
|
||||
|
||||
if self._attr_is_on != active:
|
||||
try:
|
||||
if active:
|
||||
await self.call_service(self._service_activate)
|
||||
_LOGGER.info("%s - central boiler have been turned on", self)
|
||||
else:
|
||||
await self.call_service(self._service_deactivate)
|
||||
_LOGGER.info("%s - central boiler have been turned off", self)
|
||||
self._attr_is_on = active
|
||||
send_vtherm_event(
|
||||
hass=self._hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=self,
|
||||
data={"central_boiler": active},
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(
|
||||
"%s - Impossible to activate/deactivat boiler due to error %s."
|
||||
"Central boiler will not being controled by VTherm."
|
||||
"Please check your service configuration. Cf. README.",
|
||||
self,
|
||||
err,
|
||||
)
|
||||
|
||||
async def call_service(self, service_config: dict):
|
||||
"""Make a call to a service if correctly configured"""
|
||||
if not service_config:
|
||||
return
|
||||
|
||||
await self._hass.services.async_call(
|
||||
service_config["service_domain"],
|
||||
service_config["service_name"],
|
||||
service_data=service_config["data"],
|
||||
target={
|
||||
"entity_id": service_config["entity_id"],
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
""" Some usefull commons class """
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
@@ -10,41 +12,148 @@ from homeassistant.helpers.event import async_track_state_change_event, async_ca
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class NowClass:
|
||||
""" For testing purpose only"""
|
||||
"""For testing purpose only"""
|
||||
|
||||
@staticmethod
|
||||
def get_now(hass: HomeAssistant) -> datetime:
|
||||
""" A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""
|
||||
return datetime.now( get_tz(hass))
|
||||
return datetime.now(get_tz(hass))
|
||||
|
||||
def round_to_nearest(n:float, x: float)->float:
|
||||
""" Round a number to the nearest x (which should be decimal but not null)
|
||||
Example:
|
||||
nombre1 = 3.2
|
||||
nombre2 = 4.7
|
||||
x = 0.3
|
||||
|
||||
nombre_arrondi1 = round_to_nearest(nombre1, x)
|
||||
nombre_arrondi2 = round_to_nearest(nombre2, x)
|
||||
def round_to_nearest(n: float, x: float) -> float:
|
||||
"""Round a number to the nearest x (which should be decimal but not null)
|
||||
Example:
|
||||
nombre1 = 3.2
|
||||
nombre2 = 4.7
|
||||
x = 0.3
|
||||
|
||||
print(nombre_arrondi1) # Output: 3.3
|
||||
print(nombre_arrondi2) # Output: 4.6
|
||||
nombre_arrondi1 = round_to_nearest(nombre1, x)
|
||||
nombre_arrondi2 = round_to_nearest(nombre2, x)
|
||||
|
||||
print(nombre_arrondi1) # Output: 3.3
|
||||
print(nombre_arrondi2) # Output: 4.6
|
||||
"""
|
||||
assert x > 0
|
||||
return round(n * (1/x)) / (1/x)
|
||||
return round(n * (1 / x)) / (1 / x)
|
||||
|
||||
|
||||
def check_and_extract_service_configuration(service_config) -> dict:
|
||||
"""Raise a ServiceConfigurationError. In return you have a dict formatted like follows.
|
||||
Example if you call with 'climate.central_boiler/climate.set_temperature/temperature:10':
|
||||
{
|
||||
"service_domain": "climate",
|
||||
"service_name": "set_temperature",
|
||||
"entity_id": "climate.central_boiler",
|
||||
"entity_domain": "climate",
|
||||
"entity_name": "central_boiler",
|
||||
"data": {
|
||||
"temperature": "10"
|
||||
},
|
||||
"attribute_name": "temperature",
|
||||
"attribute_value: "10"
|
||||
}
|
||||
|
||||
For this example 'switch.central_boiler/switch.turn_off' you will have this:
|
||||
{
|
||||
"service_domain": "switch",
|
||||
"service_name": "turn_off",
|
||||
"entity_id": "switch.central_boiler",
|
||||
"entity_domain": "switch",
|
||||
"entity_name": "central_boiler",
|
||||
"data": { },
|
||||
}
|
||||
|
||||
All values are striped (white space are removed) and are string
|
||||
"""
|
||||
|
||||
ret = {}
|
||||
|
||||
if service_config is None:
|
||||
return ret
|
||||
|
||||
parties = service_config.split("/")
|
||||
if len(parties) < 2:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. Service {service_config} should be formatted with: 'entity_name/service_name[/data]'. See README for more information."
|
||||
)
|
||||
entity_id = parties[0]
|
||||
service_name = parties[1]
|
||||
|
||||
service_infos = service_name.split(".")
|
||||
if len(service_infos) != 2:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The service {service_config} should be formatted like: 'domain.service_name' (ex: 'switch.turn_on'). See README for more information."
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"service_domain": service_infos[0].strip(),
|
||||
"service_name": service_infos[1].strip(),
|
||||
}
|
||||
)
|
||||
|
||||
entity_infos = entity_id.split(".")
|
||||
if len(entity_infos) != 2:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The entity_id {entity_id} should be formatted like: 'domain.entity_name' (ex: 'switch.central_boiler_switch'). See README for more information."
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"entity_domain": entity_infos[0].strip(),
|
||||
"entity_name": entity_infos[1].strip(),
|
||||
"entity_id": entity_id.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
if len(parties) == 3:
|
||||
data = parties[2]
|
||||
if len(data) > 0:
|
||||
data_infos = None
|
||||
data_infos = data.split(":")
|
||||
if (
|
||||
len(data_infos) != 2
|
||||
or len(data_infos[0]) <= 0
|
||||
or len(data_infos[1]) <= 0
|
||||
):
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"attribute_name": data_infos[0].strip(),
|
||||
"attribute_value": data_infos[1].strip(),
|
||||
"data": {data_infos[0].strip(): data_infos[1].strip()},
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
|
||||
)
|
||||
else:
|
||||
ret.update({"data": {}})
|
||||
|
||||
_LOGGER.debug(
|
||||
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
@@ -130,7 +239,9 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
await try_find_climate(None)
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument
|
||||
async def async_my_climate_changed(
|
||||
self, event: Event
|
||||
): # pylint: disable=unused-argument
|
||||
"""Called when my climate have change
|
||||
This method aims to be overriden to take the status change
|
||||
"""
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.data_entry_flow import FlowHandler, FlowResult
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import check_and_extract_service_configuration
|
||||
|
||||
COMES_FROM = "comes_from"
|
||||
|
||||
@@ -191,6 +192,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
)
|
||||
raise NoCentralConfig(conf)
|
||||
|
||||
# Check the service for central boiler format
|
||||
if self._infos.get(CONF_ADD_CENTRAL_BOILER_CONTROL):
|
||||
for conf in [
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
]:
|
||||
try:
|
||||
check_and_extract_service_configuration(data.get(conf))
|
||||
except ServiceConfigurationError as err:
|
||||
raise ServiceConfigurationError(conf) from err
|
||||
|
||||
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)
|
||||
@@ -225,6 +237,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
errors[str(err)] = "window_open_detection_method"
|
||||
except NoCentralConfig as err:
|
||||
errors[str(err)] = "no_central_config"
|
||||
except ServiceConfigurationError as err:
|
||||
errors[str(err)] = "service_configuration_format"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -263,7 +277,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME
|
||||
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
|
||||
next_step = self.async_step_tpi
|
||||
if user_input and user_input.get(CONF_ADD_CENTRAL_BOILER_CONTROL) is True:
|
||||
next_step = self.async_step_central_boiler
|
||||
else:
|
||||
next_step = self.async_step_tpi
|
||||
elif user_input and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_main
|
||||
schema = STEP_MAIN_DATA_SCHEMA
|
||||
@@ -278,7 +295,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the specific main flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_spec_main user_input=%s", user_input)
|
||||
|
||||
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
|
||||
else:
|
||||
schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA
|
||||
next_step = self.async_step_type
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_main"
|
||||
@@ -286,6 +306,19 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
# This will return to async_step_main (to keep the "main" step)
|
||||
return await self.generic_step("main", schema, user_input, next_step)
|
||||
|
||||
async def async_step_central_boiler(
|
||||
self, user_input: dict | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the central boiler flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into ConfigFlow.async_step_central_boiler user_input=%s", user_input
|
||||
)
|
||||
|
||||
schema = STEP_CENTRAL_BOILER_SCHEMA
|
||||
next_step = self.async_step_tpi
|
||||
|
||||
return await self.generic_step("central_boiler", schema, user_input, next_step)
|
||||
|
||||
async def async_step_type(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the Type flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_type user_input=%s", user_input)
|
||||
@@ -364,7 +397,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
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 = schema_ac_or_not
|
||||
schema = STEP_PRESETS_DATA_SCHEMA
|
||||
|
||||
return await self.generic_step("presets", schema, user_input, next_step)
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_THERMOSTAT_TYPES, translation_key="thermostat_type"
|
||||
options=CONF_THERMOSTAT_TYPES,
|
||||
translation_key="thermostat_type",
|
||||
mode="list",
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -42,11 +44,13 @@ 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_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_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -57,6 +61,26 @@ 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,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
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),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CENTRAL_BOILER_ACTIVATION_SRV, default=""): str,
|
||||
vol.Optional(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -105,6 +129,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_REGULATION_MODES,
|
||||
translation_key="auto_regulation_mode",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float),
|
||||
@@ -115,6 +140,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_FAN_MODES,
|
||||
translation_key="auto_fan_mode",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
@@ -140,6 +166,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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,12 +220,30 @@ STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD, default=3): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION, default=30): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -216,11 +262,19 @@ STEP_CENTRAL_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int,
|
||||
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
|
||||
CONF_PRESETS_SELECTIONABLE
|
||||
vol.Optional(CONF_MOTION_PRESET, default="comfort"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_PRESETS_SELECTIONABLE,
|
||||
translation_key="presets",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): vol.In(
|
||||
CONF_PRESETS_SELECTIONABLE
|
||||
vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_PRESETS_SELECTIONABLE,
|
||||
translation_key="presets",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""Constants for the Versatile Thermostat integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from enum import Enum
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
|
||||
@@ -18,6 +20,8 @@ from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
|
||||
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
|
||||
@@ -35,7 +39,13 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
|
||||
|
||||
DOMAIN = "versatile_thermostat"
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
Platform.BINARY_SENSOR,
|
||||
]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
@@ -96,13 +106,17 @@ CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
|
||||
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
|
||||
CONF_INVERSE_SWITCH = "inverse_switch_command"
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
CONF_AUTO_FAN_MODE = "auto_fan_mode"
|
||||
CONF_AUTO_FAN_NONE = "auto_fan_none"
|
||||
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"
|
||||
CONF_SAFETY_MODE = "safety_mode"
|
||||
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
|
||||
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
|
||||
@@ -113,6 +127,15 @@ CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config"
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
|
||||
|
||||
CONF_USE_CENTRAL_MODE = "use_central_mode"
|
||||
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL = "add_central_boiler_control"
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service"
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
|
||||
|
||||
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
|
||||
CONF_WINDOW_ACTION = "window_action"
|
||||
|
||||
DEFAULT_SHORT_EMA_PARAMS = {
|
||||
"max_alpha": 0.5,
|
||||
# In sec
|
||||
@@ -242,6 +265,13 @@ ALL_CONF = (
|
||||
CONF_USE_POWER_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_MODE,
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL,
|
||||
CONF_USED_BY_CENTRAL_BOILER,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
CONF_WINDOW_ACTION,
|
||||
CONF_STEP_TEMPERATURE,
|
||||
]
|
||||
+ CONF_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
@@ -277,6 +307,18 @@ CONF_AUTO_FAN_MODES = [
|
||||
CONF_AUTO_FAN_TURBO,
|
||||
]
|
||||
|
||||
CONF_WINDOW_TURN_OFF = "window_turn_off"
|
||||
CONF_WINDOW_FAN_ONLY = "window_fan_only"
|
||||
CONF_WINDOW_FROST_TEMP = "window_frost_temp"
|
||||
CONF_WINDOW_ECO_TEMP = "window_eco_temp"
|
||||
|
||||
CONF_WINDOW_ACTIONS = [
|
||||
CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_FAN_ONLY,
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
@@ -297,6 +339,19 @@ AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"]
|
||||
|
||||
CENTRAL_CONFIG_NAME = "Central configuration"
|
||||
|
||||
CENTRAL_MODE_AUTO = "Auto"
|
||||
CENTRAL_MODE_STOPPED = "Stopped"
|
||||
CENTRAL_MODE_HEAT_ONLY = "Heat only"
|
||||
CENTRAL_MODE_COOL_ONLY = "Cool only"
|
||||
CENTRAL_MODE_FROST_PROTECTION = "Frost protection"
|
||||
CENTRAL_MODES = [
|
||||
CENTRAL_MODE_AUTO,
|
||||
CENTRAL_MODE_STOPPED,
|
||||
CENTRAL_MODE_HEAT_ONLY,
|
||||
CENTRAL_MODE_COOL_ONLY,
|
||||
CENTRAL_MODE_FROST_PROTECTION,
|
||||
]
|
||||
|
||||
|
||||
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
|
||||
class RegulationParamSlow:
|
||||
@@ -372,10 +427,20 @@ class EventType(Enum):
|
||||
POWER_EVENT: str = "versatile_thermostat_power_event"
|
||||
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
|
||||
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
|
||||
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
|
||||
PRESET_EVENT: str = "versatile_thermostat_preset_event"
|
||||
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
|
||||
|
||||
|
||||
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
|
||||
"""Send an event"""
|
||||
_LOGGER.info("%s - Sending event %s with data: %s", entity, event_type, data)
|
||||
data["entity_id"] = entity.entity_id
|
||||
data["name"] = entity.name
|
||||
data["state_attributes"] = entity.state_attributes
|
||||
hass.bus.fire(event_type.value, data)
|
||||
|
||||
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
"""Error to indicate there is an unknown entity_id given."""
|
||||
|
||||
@@ -388,6 +453,10 @@ class NoCentralConfig(HomeAssistantError):
|
||||
"""Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found"""
|
||||
|
||||
|
||||
class ServiceConfigurationError(HomeAssistantError):
|
||||
"""Error in the service configuration to control the central boiler"""
|
||||
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
"""An annotation to inform overrides"""
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "4.3.0",
|
||||
"version": "5.4.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
117
custom_components/versatile_thermostat/number.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
|
||||
# from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, CoreState # , callback
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberMode
|
||||
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 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,
|
||||
overrides,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat selects with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
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 = [
|
||||
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data),
|
||||
]
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity):
|
||||
"""Representation of the threshold of the number of VTherm
|
||||
which should be active to activate the boiler"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
self._hass = hass
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._attr_name = "Boiler Activation threshold"
|
||||
self._attr_unique_id = "boiler_activation_threshold"
|
||||
self._attr_value = self._attr_native_value = 1 # default value
|
||||
self._attr_native_min_value = 1
|
||||
self._attr_native_max_value = 9
|
||||
self._attr_step = 1 # default value
|
||||
self._attr_mode = NumberMode.AUTO
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if isinstance(self._attr_native_value, int):
|
||||
val = int(self._attr_native_value)
|
||||
return f"mdi:numeric-{val}-box-outline"
|
||||
else:
|
||||
return "mdi:numeric-0-box-outline"
|
||||
|
||||
@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()
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
api.register_central_boiler_activation_number_threshold(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
|
||||
)
|
||||
if old_state is not None:
|
||||
self._attr_value = self._attr_native_value = int(float(old_state.state))
|
||||
|
||||
@overrides
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Change the value"""
|
||||
int_value = int(value)
|
||||
old_value = int(self._attr_native_value)
|
||||
|
||||
if int_value == old_value:
|
||||
return
|
||||
|
||||
self._attr_value = self._attr_native_value = int_value
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
@@ -140,27 +140,27 @@ class PropAlgorithm:
|
||||
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
|
||||
|
||||
def set_security(self, default_on_percent: float):
|
||||
"""Set a default value for on_percent (used for security mode)"""
|
||||
"""Set a default value for on_percent (used for safety mode)"""
|
||||
self._security = True
|
||||
self._default_on_percent = default_on_percent
|
||||
self._calculate_internal()
|
||||
|
||||
def unset_security(self):
|
||||
"""Unset the security mode"""
|
||||
"""Unset the safety mode"""
|
||||
self._security = False
|
||||
self._calculate_internal()
|
||||
|
||||
@property
|
||||
def on_percent(self) -> float:
|
||||
"""Returns the percentage the heater must be ON
|
||||
In security mode this value is overriden with the _default_on_percent
|
||||
In safety mode this value is overriden with the _default_on_percent
|
||||
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
|
||||
return round(self._on_percent, 2)
|
||||
|
||||
@property
|
||||
def calculated_on_percent(self) -> float:
|
||||
"""Returns the calculated percentage the heater must be ON
|
||||
Calculated means NOT overriden even in security mode
|
||||
Calculated means NOT overriden even in safety mode
|
||||
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
|
||||
return round(self._calculated_on_percent, 2)
|
||||
|
||||
|
||||
138
custom_components/versatile_thermostat/select.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import (
|
||||
BaseThermostat,
|
||||
ConfigData,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CENTRAL_MODE_AUTO,
|
||||
CENTRAL_MODES,
|
||||
overrides,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat selects with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
|
||||
entities = [
|
||||
CentralModeSelect(hass, unique_id, name, entry.data),
|
||||
]
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
"""Representation of the central mode choice"""
|
||||
|
||||
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)
|
||||
self._attr_name = "Central Mode"
|
||||
self._attr_unique_id = "central_mode"
|
||||
self._attr_options = CENTRAL_MODES
|
||||
self._attr_current_option = CENTRAL_MODE_AUTO
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return "mdi:form-select"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._config_id)},
|
||||
name=self._device_name,
|
||||
manufacturer=DEVICE_MANUFACTURER,
|
||||
model=DOMAIN,
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
old_state = await self.async_get_last_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling async_added_to_hass old_state is %s", self, old_state
|
||||
)
|
||||
if old_state is not None:
|
||||
self._attr_current_option = old_state.state
|
||||
|
||||
@callback
|
||||
async def _async_startup_internal(*_):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
await self.notify_central_mode_change()
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
await _async_startup_internal()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
old_option = self._attr_current_option
|
||||
|
||||
if option == old_option:
|
||||
return
|
||||
|
||||
if option in CENTRAL_MODES:
|
||||
self._attr_current_option = option
|
||||
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
# Update all VTherm states
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if isinstance(entity, BaseThermostat):
|
||||
_LOGGER.debug(
|
||||
"Changing the central_mode. We have find %s to update",
|
||||
entity.name,
|
||||
)
|
||||
await entity.check_central_mode(
|
||||
self._attr_current_option, old_central_mode
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
@@ -3,9 +3,15 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.core import HomeAssistant, callback, Event, CoreState
|
||||
|
||||
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE
|
||||
from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
UnitOfPower,
|
||||
UnitOfEnergy,
|
||||
PERCENTAGE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -16,9 +22,24 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PROP_FUNCTION,
|
||||
@@ -28,6 +49,8 @@ from .const import (
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL,
|
||||
overrides,
|
||||
)
|
||||
|
||||
THRESHOLD_WATT_KILO = 100
|
||||
@@ -49,35 +72,43 @@ async def async_setup_entry(
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
entities = None
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
if entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL):
|
||||
entities = [
|
||||
NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data)
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
else:
|
||||
entities = [
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
|
||||
EMATemperatureSensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]:
|
||||
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
entities = [
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
|
||||
EMATemperatureSensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]:
|
||||
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
|
||||
entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
|
||||
entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(
|
||||
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
|
||||
)
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(RegulatedTemperatureSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@@ -253,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
|
||||
@@ -597,3 +628,116 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
|
||||
class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
"""Representation of the threshold of the number of VTherm
|
||||
which should be active to activate the boiler"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
self._hass = hass
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._attr_name = "Nb device active for boiler"
|
||||
self._attr_unique_id = "nb_device_active_boiler"
|
||||
self._attr_value = self._attr_native_value = None # default value
|
||||
self._entities = []
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:heat-wave"
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 0
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
api.register_nb_device_active_boiler(self)
|
||||
|
||||
@callback
|
||||
async def _async_startup_internal(*_):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
await self.listen_vtherms_entities()
|
||||
|
||||
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_vtherms_entities(self):
|
||||
"""Initialize the listening of state change of VTherms"""
|
||||
|
||||
# Listen to all VTherm state change
|
||||
self._entities = []
|
||||
underlying_entities_id = []
|
||||
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
|
||||
self._entities.append(entity)
|
||||
for under in entity.underlying_entities:
|
||||
underlying_entities_id.append(under.entity_id)
|
||||
if len(underlying_entities_id) > 0:
|
||||
# Arme l'écoute de la première entité
|
||||
listener_cancel = async_track_state_change_event(
|
||||
self._hass,
|
||||
underlying_entities_id,
|
||||
self.calculate_nb_active_devices,
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - the underlyings that could controls the central boiler are %s",
|
||||
self,
|
||||
underlying_entities_id,
|
||||
)
|
||||
self.async_on_remove(listener_cancel)
|
||||
else:
|
||||
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
|
||||
|
||||
await self.calculate_nb_active_devices(None)
|
||||
|
||||
async def calculate_nb_active_devices(self, _):
|
||||
"""Calculate the number of active VTherm that have an
|
||||
influence on central boiler"""
|
||||
|
||||
_LOGGER.debug("%s - calculating the number of active VTherm", self)
|
||||
nb_active = 0
|
||||
for entity in self._entities:
|
||||
_LOGGER.debug(
|
||||
"Examining the hvac_action of %s",
|
||||
entity.name,
|
||||
)
|
||||
if (
|
||||
entity.hvac_mode == HVACMode.HEAT
|
||||
and entity.hvac_action == HVACAction.HEATING
|
||||
):
|
||||
for under in entity.underlying_entities:
|
||||
nb_active += 1 if under.is_device_active else 0
|
||||
|
||||
self._attr_native_value = nb_active
|
||||
self.async_write_ha_state()
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -23,15 +23,18 @@
|
||||
"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"
|
||||
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -75,7 +78,7 @@
|
||||
"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",
|
||||
"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"
|
||||
@@ -121,27 +124,29 @@
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration"
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
@@ -161,7 +166,7 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
@@ -177,7 +182,7 @@
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"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",
|
||||
@@ -253,15 +258,18 @@
|
||||
"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"
|
||||
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -305,7 +313,7 @@
|
||||
"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",
|
||||
"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"
|
||||
@@ -358,20 +366,22 @@
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration"
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion - {name}",
|
||||
"description": "Motion management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
@@ -391,7 +401,7 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power - {name}",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
@@ -407,7 +417,7 @@
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence - {name}",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"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",
|
||||
@@ -454,7 +464,8 @@
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -487,6 +498,22 @@
|
||||
"auto_fan_high": "High",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Turn off",
|
||||
"window_fan_only": "Fan only",
|
||||
"window_frost_temp": "Frost protect",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Frost protect",
|
||||
"eco": "Eco",
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -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 (
|
||||
@@ -59,19 +60,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(
|
||||
@@ -94,7 +95,9 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
)
|
||||
|
||||
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 +130,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)
|
||||
|
||||
@@ -136,6 +139,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ")
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
|
||||
self,
|
||||
@@ -157,7 +165,6 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
|
||||
_LOGGER.info("%s - regulation calculation will be done", self)
|
||||
self._last_regulation_change = now
|
||||
|
||||
new_regulated_temp = round_to_nearest(
|
||||
self._regulation_algo.calculate_regulated_temperature(
|
||||
@@ -183,6 +190,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
new_regulated_temp,
|
||||
)
|
||||
|
||||
self._last_regulation_change = now
|
||||
for under in self._underlyings:
|
||||
await under.set_temperature(
|
||||
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
|
||||
@@ -234,7 +242,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)
|
||||
@@ -276,7 +284,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode):
|
||||
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:
|
||||
@@ -352,7 +360,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
|
||||
@@ -364,7 +372,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
|
||||
@@ -422,10 +430,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:
|
||||
@@ -521,7 +530,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return
|
||||
|
||||
added_energy = 0
|
||||
if self.is_over_climate and self._underlying_climate_delta_t is not None:
|
||||
if (
|
||||
self.is_over_climate
|
||||
and self._underlying_climate_delta_t is not None
|
||||
and self._device_power
|
||||
):
|
||||
added_energy = self._device_power * self._underlying_climate_delta_t
|
||||
|
||||
self._total_energy += added_energy
|
||||
@@ -533,7 +546,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
|
||||
@@ -543,7 +556,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()
|
||||
@@ -596,8 +609,9 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
# new_hvac_mode = HVACMode.OFF
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
||||
"%s - Underlying climate %s changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
||||
self,
|
||||
new_state.entity_id,
|
||||
new_hvac_mode,
|
||||
self._hvac_mode,
|
||||
new_hvac_action,
|
||||
@@ -653,7 +667,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
changes = True
|
||||
|
||||
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change.
|
||||
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
|
||||
# In that case a loop is possible if a user change multiple times during this 6 sec.
|
||||
if new_state_date_updated and self._last_change_time:
|
||||
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
||||
@@ -679,12 +693,31 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
]
|
||||
and self._hvac_mode != new_hvac_mode
|
||||
):
|
||||
changes = True
|
||||
self._hvac_mode = new_hvac_mode
|
||||
# Update all underlyings state
|
||||
# Issue #334 - if all underlyings are not aligned with the same hvac_mode don't change the underlying and wait they are aligned
|
||||
if self.is_over_climate:
|
||||
for under in self._underlyings:
|
||||
if (
|
||||
under.entity_id != new_state.entity_id
|
||||
and under.hvac_mode != self._hvac_mode
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - the underlying's hvac_mode %s is not aligned with VTherm hvac_mode %s. So we don't diffuse the change to all other underlyings to avoid loops",
|
||||
under,
|
||||
under.hvac_mode,
|
||||
self._hvac_mode,
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - All underlyings have the same hvac_mode, so VTherm will send the new hvac mode %s",
|
||||
self,
|
||||
new_hvac_mode,
|
||||
)
|
||||
for under in self._underlyings:
|
||||
await under.set_hvac_mode(new_hvac_mode)
|
||||
changes = True
|
||||
self._hvac_mode = new_hvac_mode
|
||||
|
||||
# A quick win to known if it has change by using the self._attr_fan_mode and not only underlying[0].fan_mode
|
||||
if new_fan_mode != self._attr_fan_mode:
|
||||
@@ -716,7 +749,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, _)
|
||||
|
||||
@@ -728,27 +761,27 @@ 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 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
|
||||
@@ -821,13 +854,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:
|
||||
@@ -914,7 +948,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:
|
||||
@@ -947,7 +981,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:
|
||||
@@ -976,7 +1010,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 (
|
||||
@@ -15,7 +19,7 @@ from .const import (
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .underlyings import UnderlyingSwitch
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
@@ -51,7 +55,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 +76,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)
|
||||
@@ -200,7 +204,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")
|
||||
@@ -208,5 +212,6 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
return
|
||||
if old_state is None:
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
# 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 CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, overrides
|
||||
from .const import (
|
||||
CONF_VALVE,
|
||||
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,
|
||||
)
|
||||
|
||||
from .underlyings import UnderlyingValve
|
||||
|
||||
@@ -38,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:
|
||||
@@ -59,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,
|
||||
@@ -115,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.
|
||||
"""
|
||||
@@ -158,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(
|
||||
@@ -171,7 +215,21 @@ 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,
|
||||
@@ -179,9 +237,34 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -23,15 +23,18 @@
|
||||
"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"
|
||||
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -75,7 +78,7 @@
|
||||
"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",
|
||||
"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"
|
||||
@@ -121,27 +124,29 @@
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration"
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
@@ -161,7 +166,7 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
@@ -177,7 +182,7 @@
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"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",
|
||||
@@ -253,15 +258,18 @@
|
||||
"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"
|
||||
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
@@ -305,7 +313,7 @@
|
||||
"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",
|
||||
"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"
|
||||
@@ -358,20 +366,22 @@
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration"
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion - {name}",
|
||||
"description": "Motion management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
@@ -391,7 +401,7 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power - {name}",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
@@ -407,7 +417,7 @@
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence - {name}",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"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",
|
||||
@@ -454,7 +464,8 @@
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -487,6 +498,22 @@
|
||||
"auto_fan_high": "High",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Turn off",
|
||||
"window_fan_only": "Fan only",
|
||||
"window_frost_temp": "Frost protect",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Frost protect",
|
||||
"eco": "Eco",
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -23,16 +23,19 @@
|
||||
"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 principale"
|
||||
"use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
|
||||
"add_central_boiler_control": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
|
||||
},
|
||||
"data_description": {
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée",
|
||||
"use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale"
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure."
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -75,7 +78,7 @@
|
||||
"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",
|
||||
"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"
|
||||
@@ -121,37 +124,39 @@
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures"
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures"
|
||||
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures",
|
||||
"window_action": "Action a effectuer si la fenêtre est détectée comme ouverte"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestion de la détection de mouvement",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement",
|
||||
"motion_delay": "Délai d'activation",
|
||||
"motion_off_delay": "Délai de désactivation",
|
||||
"motion_preset": "Preset si mouvement",
|
||||
"no_motion_preset": "Preset si pas de mouvement",
|
||||
"no_motion_preset": "Preset sans mouvement",
|
||||
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
|
||||
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
|
||||
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
|
||||
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||
@@ -177,7 +182,7 @@
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestion de la présence",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence",
|
||||
"eco_away_temp": "preset Eco",
|
||||
@@ -190,7 +195,7 @@
|
||||
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
|
||||
"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",
|
||||
@@ -218,6 +223,18 @@
|
||||
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Contrôle de la chaudière centrale",
|
||||
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Commande pour allumer",
|
||||
"central_boiler_deactivation_service": "Commande pour éteindre"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -253,16 +270,19 @@
|
||||
"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"
|
||||
"use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
|
||||
"add_central_boiler_control": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
|
||||
},
|
||||
"data_description": {
|
||||
"use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique",
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée"
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée."
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -305,7 +325,7 @@
|
||||
"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",
|
||||
"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"
|
||||
@@ -345,22 +365,24 @@
|
||||
},
|
||||
"window": {
|
||||
"title": "Ouverture - {name}",
|
||||
"description": "Gestion des ouvertures. Coupe le radiateur si l'ouverture est ouverte.",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures"
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures"
|
||||
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures",
|
||||
"window_action": "Action a effectuer si la fenêtre est détectée comme ouverte"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -371,11 +393,11 @@
|
||||
"motion_delay": "Délai d'activation",
|
||||
"motion_off_delay": "Délai de désactivation",
|
||||
"motion_preset": "Preset si mouvement",
|
||||
"no_motion_preset": "Preset si pas de mouvement",
|
||||
"no_motion_preset": "Preset sans mouvement",
|
||||
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
|
||||
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
|
||||
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
|
||||
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||
@@ -401,7 +423,7 @@
|
||||
},
|
||||
"presence": {
|
||||
"title": "Présence - {name}",
|
||||
"description": "Gestion de la présence. Le capteur de présence doit être true ou home si quelqu'un est présent.",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence",
|
||||
"eco_away_temp": "preset Eco",
|
||||
@@ -414,7 +436,7 @@
|
||||
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
|
||||
"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",
|
||||
@@ -442,13 +464,26 @@
|
||||
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Contrôle de la chaudière centrale - {name}",
|
||||
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Commande pour allumer",
|
||||
"central_boiler_deactivation_service": "Commande pour éteindre"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu",
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser."
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
|
||||
"service_configuration_format": "Mauvais format de la configuration du service"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -481,6 +516,22 @@
|
||||
"auto_fan_high": "Forte",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Eteindre",
|
||||
"window_fan_only": "Ventilateur seul",
|
||||
"window_frost_temp": "Hors gel",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Hors-gel",
|
||||
"eco": "Eco",
|
||||
"comfort": "Confort",
|
||||
"boost": "Renforcé (boost)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
"flow_title": "Všestranná konfigurácia termostatu",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Typ všestranného termostatu",
|
||||
"data": {
|
||||
"thermostat_type": "Typ termostatu"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Len jeden centrálny typ konfigurácie je možný"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Pridajte nový všestranný termostat",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
"data": {
|
||||
@@ -15,10 +24,17 @@
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"device_power": "Napájanie zariadenia",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti"
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
|
||||
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú hlavnú konfiguráciu pre tento VTherm",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -39,11 +55,11 @@
|
||||
"valve_entity2_id": "2. ventil číslo",
|
||||
"valve_entity3_id": "3. ventil číslo",
|
||||
"valve_entity4_id": "4. ventil číslo",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "ID entity povinného ohrievača",
|
||||
@@ -60,11 +76,11 @@
|
||||
"valve_entity2_id": "2. ventil číslo entity id",
|
||||
"valve_entity3_id": "3. ventil číslo entity id",
|
||||
"valve_entity4_id": "4. ventil číslo 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 send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -72,7 +88,13 @@
|
||||
"description": "Časovo proporcionálne integrálne atribúty",
|
||||
"data": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty"
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
|
||||
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
|
||||
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
@@ -85,7 +107,18 @@
|
||||
"frost_temp": "Teplota v prednastavení Frost protection",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
},
|
||||
"data_description": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -96,14 +129,16 @@
|
||||
"window_delay": "Oneskorenie snímača okna (sekundy)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -114,14 +149,16 @@
|
||||
"motion_delay": "Oneskorenie aktivácie",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie",
|
||||
"motion_preset": "Prednastavený pohyb",
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu"
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu",
|
||||
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "ID entity snímača pohybu",
|
||||
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
|
||||
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb"
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
|
||||
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
@@ -130,7 +167,14 @@
|
||||
"data": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu"
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
@@ -144,7 +188,19 @@
|
||||
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -154,20 +210,23 @@
|
||||
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
|
||||
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"security_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
|
||||
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime"
|
||||
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
|
||||
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave"
|
||||
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Neočakávaná chyba",
|
||||
"unknown_entity": "Neznáme ID entity",
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje"
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované"
|
||||
@@ -177,7 +236,16 @@
|
||||
"flow_title": "Všestranná konfigurácia termostatu",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Pridajte nový všestranný termostat",
|
||||
"title": "Typ - {name}",
|
||||
"data": {
|
||||
"thermostat_type": "Typ termostatu"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Je možný len jeden centrálny typ konfigurácie"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Hlavný - {name}",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
"data": {
|
||||
"name": "Názov",
|
||||
@@ -188,14 +256,21 @@
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"device_power": "Výkon zariadenia (kW)",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti"
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
|
||||
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Ak chcete použiť špecifickú konfiguráciu pre tento VTherm, zrušte začiarknutie",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Prepojené entity",
|
||||
"title": "Prepojené entity - {name}",
|
||||
"description": "Atribúty prepojených entít",
|
||||
"data": {
|
||||
"heater_entity_id": "Spínač ohrievača",
|
||||
@@ -212,11 +287,11 @@
|
||||
"valve_entity2_id": "2. ventil číslo",
|
||||
"valve_entity3_id": "3. ventil číslo",
|
||||
"valve_entity4_id": "4. ventil číslo",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": " Auto fan mode"
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "ID entity povinného ohrievača",
|
||||
@@ -233,23 +308,29 @@
|
||||
"valve_entity2_id": "2. ventil číslo entity id",
|
||||
"valve_entity3_id": "3. ventil číslo entity id",
|
||||
"valve_entity4_id": "4. ventil číslo 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 send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"title": "TPI - {name}",
|
||||
"description": "Časovo proporcionálne integrálne atribúty",
|
||||
"data": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu"
|
||||
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu",
|
||||
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
|
||||
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Predvoľby",
|
||||
"title": "Predvoľby - {name}",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
@@ -258,52 +339,74 @@
|
||||
"frost_temp": "Teplota v prednastavení Frost protection",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
},
|
||||
"data_description": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Správa okien",
|
||||
"title": "Správa okien - {name}",
|
||||
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "ID entity snímača okna",
|
||||
"window_delay": "Oneskorenie snímača okna (sekundy)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Riadenie pohybu",
|
||||
"title": "Riadenie pohybu - {name}",
|
||||
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "ID entity snímača pohybu",
|
||||
"motion_delay": "Oneskorenie aktivácie",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie",
|
||||
"motion_preset": "Prednastavený pohyb",
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu"
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu",
|
||||
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "ID entity snímača pohybu",
|
||||
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
|
||||
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb"
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
|
||||
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Správa napájania",
|
||||
"title": "Správa napájania - {name}",
|
||||
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu"
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
@@ -317,30 +420,45 @@
|
||||
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Pokročilé parametre",
|
||||
"title": "Pokročilé parametre - {name}",
|
||||
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
|
||||
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"security_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
|
||||
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime"
|
||||
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
|
||||
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave"
|
||||
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Neočakávaná chyba",
|
||||
"unknown_entity": "Neznáme ID entity",
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje"
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované"
|
||||
@@ -349,27 +467,28 @@
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_central_config": "Centrálna konfigurácia",
|
||||
"thermostat_over_switch": "Termostat nad spínačom",
|
||||
"thermostat_over_climate": "Termostat nad iným termostatom",
|
||||
"thermostat_over_valve": "Thermostat over a valve"
|
||||
"thermostat_over_valve": "Termostat nad ventilom"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
"auto_regulation_slow": "Pomalé",
|
||||
"auto_regulation_strong": "Silné",
|
||||
"auto_regulation_medium": "Stredné",
|
||||
"auto_regulation_light": "Jemné",
|
||||
"auto_regulation_expert": "Expertné",
|
||||
"auto_regulation_none": "Nie auto-regulácia"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "No auto-fan",
|
||||
"auto_fan_low": "Low",
|
||||
"auto_fan_medium": "Medium",
|
||||
"auto_fan_high": "High",
|
||||
"auto_fan_none": "Žiadny automatický ventilátor",
|
||||
"auto_fan_low": "Nízky",
|
||||
"auto_fan_medium": "Stredný",
|
||||
"auto_fan_high": "Vysoký",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
}
|
||||
@@ -389,4 +508,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +133,17 @@ class UnderlyingEntity:
|
||||
async def check_initial_state(self, hvac_mode: HVACMode):
|
||||
"""Prevent the underlying to be on but thermostat is off"""
|
||||
if hvac_mode == HVACMode.OFF and self.is_device_active:
|
||||
_LOGGER.warning(
|
||||
_LOGGER.info(
|
||||
"%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s",
|
||||
self,
|
||||
self._entity_id,
|
||||
)
|
||||
await self.set_hvac_mode(hvac_mode)
|
||||
elif hvac_mode != HVACMode.OFF and not self.is_device_active:
|
||||
_LOGGER.warning(
|
||||
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s",
|
||||
_LOGGER.info(
|
||||
"%s - The hvac mode is %s, but the underlying device is not ON. Turning on device %s if needed",
|
||||
self,
|
||||
hvac_mode,
|
||||
self._entity_id,
|
||||
)
|
||||
await self.set_hvac_mode(hvac_mode)
|
||||
@@ -354,8 +355,8 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
if await self._thermostat.check_overpowering():
|
||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||
return
|
||||
# Security mode could have change the on_time percent
|
||||
await self._thermostat.check_security()
|
||||
# safety mode could have change the on_time percent
|
||||
await self._thermostat.check_safety()
|
||||
time = self._on_time_sec
|
||||
|
||||
action_label = "start"
|
||||
@@ -483,6 +484,14 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
if self._underlying_climate.hvac_mode == hvac_mode:
|
||||
_LOGGER.debug(
|
||||
"%s - hvac_mode is already is requested state %s. Do not send any command",
|
||||
self,
|
||||
self._underlying_climate.hvac_mode,
|
||||
)
|
||||
return False
|
||||
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -690,7 +699,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
min_val = self._underlying_climate.min_temp
|
||||
max_val = self._underlying_climate.max_temp
|
||||
|
||||
new_value = round(max(min_val, min(value, max_val)))
|
||||
new_value = max(min_val, min(value, max_val))
|
||||
else:
|
||||
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||
new_value = value
|
||||
@@ -749,19 +758,25 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
|
||||
self._percent_open = 0
|
||||
if self.is_device_active:
|
||||
# Issue 341
|
||||
is_active = self.is_device_active
|
||||
self._percent_open = self.cap_sent_value(0)
|
||||
if is_active:
|
||||
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:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if hvac_mode == HVACMode.OFF and self.is_device_active:
|
||||
await self.turn_off()
|
||||
|
||||
if hvac_mode != HVACMode.OFF and not self.is_device_active:
|
||||
await self.turn_on()
|
||||
|
||||
if self._hvac_mode != hvac_mode:
|
||||
self._hvac_mode = hvac_mode
|
||||
return True
|
||||
@@ -790,6 +805,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
if force:
|
||||
self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
await self.send_percent_open()
|
||||
|
||||
@overrides
|
||||
|
||||
@@ -7,6 +7,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_SHORT_EMA_PARAMS,
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
)
|
||||
@@ -20,7 +21,6 @@ class VersatileThermostatAPI(dict):
|
||||
"""The VersatileThermostatAPI"""
|
||||
|
||||
_hass: HomeAssistant = None
|
||||
# _entries: Dict(str, ConfigEntry)
|
||||
|
||||
@classmethod
|
||||
def get_vtherm_api(cls, hass=None):
|
||||
@@ -47,6 +47,10 @@ class VersatileThermostatAPI(dict):
|
||||
super().__init__()
|
||||
self._expert_params = None
|
||||
self._short_ema_params = None
|
||||
self._safety_mode = None
|
||||
self._central_boiler_entity = None
|
||||
self._threshold_number_entity = None
|
||||
self._nb_active_number_entity = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
@@ -64,14 +68,12 @@ class VersatileThermostatAPI(dict):
|
||||
def add_entry(self, entry: ConfigEntry):
|
||||
"""Add a new entry"""
|
||||
_LOGGER.debug("Add the entry %s", entry.entry_id)
|
||||
# self._entries[entry.entry_id] = entry
|
||||
# Add the entry in hass.data
|
||||
VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry
|
||||
|
||||
def remove_entry(self, entry: ConfigEntry):
|
||||
"""Remove an entry"""
|
||||
_LOGGER.debug("Remove the entry %s", entry.entry_id)
|
||||
# self._entries.pop(entry.entry_id)
|
||||
VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# If not more entries are preset, remove the API
|
||||
if len(self) == 0:
|
||||
@@ -90,6 +92,30 @@ class VersatileThermostatAPI(dict):
|
||||
if self._short_ema_params:
|
||||
_LOGGER.debug("We have found short ema params %s", self._short_ema_params)
|
||||
|
||||
self._safety_mode = config.get(CONF_SAFETY_MODE)
|
||||
if self._safety_mode:
|
||||
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
|
||||
|
||||
def register_central_boiler(self, central_boiler_entity):
|
||||
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
|
||||
class to register itself at creation"""
|
||||
self._central_boiler_entity = central_boiler_entity
|
||||
|
||||
def register_central_boiler_activation_number_threshold(
|
||||
self, threshold_number_entity
|
||||
):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
self._threshold_number_entity = threshold_number_entity
|
||||
|
||||
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
|
||||
|
||||
async def reload_central_boiler_entities_list(self):
|
||||
"""Reload the central boiler list of entities if a central boiler is used"""
|
||||
if self._nb_active_number_entity is not None:
|
||||
await self._nb_active_number_entity.listen_vtherms_entities()
|
||||
|
||||
@property
|
||||
def self_regulation_expert(self):
|
||||
"""Get the self regulation params"""
|
||||
@@ -97,9 +123,48 @@ class VersatileThermostatAPI(dict):
|
||||
|
||||
@property
|
||||
def short_ema_params(self):
|
||||
"""Get the self regulation params"""
|
||||
"""Get the short EMA params in expert mode"""
|
||||
return self._short_ema_params
|
||||
|
||||
@property
|
||||
def safety_mode(self):
|
||||
"""Get the safety_mode params"""
|
||||
return self._safety_mode
|
||||
|
||||
@property
|
||||
def central_boiler_entity(self):
|
||||
"""Get the central boiler binary_sensor entity"""
|
||||
return self._central_boiler_entity
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler(self):
|
||||
"""Returns the number of active VTherm which have an
|
||||
influence on boiler"""
|
||||
if self._nb_active_number_entity is None:
|
||||
return None
|
||||
else:
|
||||
return self._nb_active_number_entity.native_value
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler_entity(self):
|
||||
"""Returns the number of active VTherm entity which have an
|
||||
influence on boiler"""
|
||||
return self._nb_active_number_entity
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler_threshold_entity(self):
|
||||
"""Returns the number of active VTherm entity which have an
|
||||
influence on boiler"""
|
||||
return self._threshold_number_entity
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler_threshold(self):
|
||||
"""Returns the number of active VTherm entity which have an
|
||||
influence on boiler"""
|
||||
if self._threshold_number_entity is None:
|
||||
return None
|
||||
return int(self._threshold_number_entity.native_value)
|
||||
|
||||
@property
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
|
||||
BIN
images/central_mode.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
images/config-central-boiler-1.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/config-central-boiler-2.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 45 KiB |
BIN
images/config-main0.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/dev-tools-turnon-boiler-1.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
images/dev-tools-turnon-boiler-2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/entitites-central-boiler.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/plotly-curves.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
7
pyrightconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"include": [
|
||||
"custom_components/versatile_thermostat/**",
|
||||
"homeassistant/**"
|
||||
],
|
||||
"reportShadowedImports": false
|
||||
}
|
||||
@@ -2,5 +2,7 @@
|
||||
-r requirements_dev.txt
|
||||
aiodiscover
|
||||
ulid_transform
|
||||
pytest
|
||||
coverage
|
||||
pytest-asyncio
|
||||
pytest-homeassistant-custom-component
|
||||
153
tests/commons.py
@@ -19,6 +19,14 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
)
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
@@ -60,6 +68,7 @@ from .const import ( # pylint: disable=unused-import
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
overrides,
|
||||
)
|
||||
|
||||
|
||||
@@ -137,6 +146,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,
|
||||
@@ -168,8 +178,53 @@ FULL_CENTRAL_CONFIG = {
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
|
||||
}
|
||||
|
||||
FULL_CENTRAL_CONFIG_WITH_BOILER = {
|
||||
CONF_NAME: CENTRAL_CONFIG_NAME,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17.1,
|
||||
"comfort_temp": 0,
|
||||
"boost_temp": 19.1,
|
||||
"eco_ac_temp": 25.1,
|
||||
"comfort_ac_temp": 23.1,
|
||||
"boost_ac_temp": 21.1,
|
||||
"frost_away_temp": 15.1,
|
||||
"eco_away_temp": 15.2,
|
||||
"comfort_away_temp": 0,
|
||||
"boost_away_temp": 15.4,
|
||||
"eco_ac_away_temp": 30.5,
|
||||
"comfort_ac_away_temp": 0,
|
||||
"boost_ac_away_temp": 30.7,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 31,
|
||||
CONF_MOTION_DELAY: 31,
|
||||
CONF_MOTION_OFF_DELAY: 301,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "frost",
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
||||
CONF_PRESET_POWER: 14,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 11,
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL: True,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
|
||||
}
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -185,6 +240,7 @@ class MockClimate(ClimateEntity):
|
||||
hvac_mode: HVACMode = HVACMode.OFF,
|
||||
hvac_action: HVACAction = HVACAction.OFF,
|
||||
fan_modes: list[str] = None,
|
||||
hvac_modes: list[str] = None,
|
||||
) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
@@ -200,14 +256,24 @@ class MockClimate(ClimateEntity):
|
||||
HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
|
||||
)
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
self._attr_hvac_modes = (
|
||||
hvac_modes
|
||||
if hvac_modes is not None
|
||||
else [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
)
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_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
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""The hvac action of the mock climate"""
|
||||
@@ -239,6 +305,10 @@ class MockClimate(ClimateEntity):
|
||||
"""The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
|
||||
def set_hvac_action(self, hvac_action: HVACAction):
|
||||
"""Set the HVACaction"""
|
||||
self._attr_hvac_action = hvac_action
|
||||
@@ -345,6 +415,66 @@ class MagicMockClimate(MagicMock):
|
||||
return 19
|
||||
|
||||
|
||||
class MockSwitch(SwitchEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos={}
|
||||
):
|
||||
"""Init the switch"""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
self.platform = "switch"
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._name = name
|
||||
self._attr_is_on = False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
"""Turns the switch on and notify the state change"""
|
||||
self._attr_is_on = True
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
"""Turns the switch on and notify the state change"""
|
||||
self._attr_is_on = False
|
||||
# self.async_write_ha_state()
|
||||
|
||||
|
||||
class MockNumber(NumberEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos={}
|
||||
):
|
||||
"""Init the switch"""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
self.platform = "number"
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._name = name
|
||||
self._attr_native_value = 0
|
||||
self._attr_native_min_value = 0
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@overrides
|
||||
def set_native_value(self, value: float):
|
||||
"""Change the value"""
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
async def create_thermostat(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||
) -> BaseThermostat:
|
||||
@@ -356,13 +486,6 @@ async def create_thermostat(
|
||||
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
|
||||
|
||||
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
|
||||
|
||||
@@ -423,10 +546,12 @@ async def send_temperature_change_event(
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_temperature_changed(temp_event)
|
||||
dearm_window_auto = await entity._async_temperature_changed(temp_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return dearm_window_auto
|
||||
|
||||
|
||||
async def send_ext_temperature_change_event(
|
||||
entity: BaseThermostat, new_temp, date, sleep=True
|
||||
@@ -614,6 +739,7 @@ async def send_climate_change_event(
|
||||
old_hvac_action: HVACAction,
|
||||
date,
|
||||
sleep=True,
|
||||
underlying_entity_id: str = None,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
@@ -625,18 +751,23 @@ async def send_climate_change_event(
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
|
||||
send_from_entity_id = (
|
||||
underlying_entity_id if underlying_entity_id is not None else entity.entity_id
|
||||
)
|
||||
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=send_from_entity_id,
|
||||
state=new_hvac_mode,
|
||||
attributes={"hvac_action": new_hvac_action},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=send_from_entity_id,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
|
||||
@@ -29,7 +29,11 @@ from custom_components.versatile_thermostat.config_flow import (
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
|
||||
from .commons import create_central_config
|
||||
from .commons import (
|
||||
create_central_config,
|
||||
FULL_CENTRAL_CONFIG,
|
||||
FULL_CENTRAL_CONFIG_WITH_BOILER,
|
||||
)
|
||||
|
||||
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
|
||||
|
||||
@@ -127,6 +131,16 @@ async def init_central_config_fixture(
|
||||
hass, init_vtherm_api
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialize the VTherm API"""
|
||||
await create_central_config(hass)
|
||||
await create_central_config(hass, FULL_CENTRAL_CONFIG)
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="init_central_config_with_boiler_fixture")
|
||||
async def init_central_config_with_boiler_fixture(
|
||||
hass, init_vtherm_api
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialize the VTherm API"""
|
||||
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
|
||||
|
||||
yield
|
||||
|
||||
@@ -33,6 +33,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,
|
||||
@@ -50,7 +51,8 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: True
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
@@ -58,6 +60,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
|
||||
}
|
||||
|
||||
@@ -65,6 +68,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -151,6 +155,7 @@ MOCK_WINDOW_AUTO_CONFIG = {
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 5.0,
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
|
||||
}
|
||||
|
||||
MOCK_MOTION_CONFIG = {
|
||||
|
||||
@@ -358,7 +358,7 @@ async def test_bug_82(
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -427,7 +427,7 @@ async def test_bug_82(
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force security mode
|
||||
# Force safety mode
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (
|
||||
@@ -591,6 +591,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
|
||||
)
|
||||
|
||||
@@ -623,25 +624,34 @@ async def test_bug_272(
|
||||
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
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
|
||||
# Set the hvac_mode to HEAT
|
||||
entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
await entity.async_set_temperature(temperature=17.5)
|
||||
|
||||
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
# call.async_call(
|
||||
# "climate",
|
||||
# SERVICE_SET_HVAC_MODE,
|
||||
# {"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
|
||||
# ),
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 17,
|
||||
"temperature": 17.5,
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
@@ -661,7 +671,7 @@ async def test_bug_272(
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# In the accepted interval
|
||||
# Not in the accepted interval (15-19)
|
||||
await entity.async_set_temperature(temperature=10)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
@@ -689,7 +699,7 @@ async def test_bug_272(
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=20)
|
||||
await entity.async_set_temperature(temperature=20.8)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
|
||||
838
tests/test_central_boiler.py
Normal file
@@ -0,0 +1,838 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the central_configuration """
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_valve import (
|
||||
ThermostatOverValve,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
from custom_components.versatile_thermostat.binary_sensor import (
|
||||
CentralBoilerBinarySensor,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_add_a_central_config_with_boiler(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the clean_central_config_doubon of base_thermostat"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
data=FULL_CENTRAL_CONFIG_WITH_BOILER,
|
||||
)
|
||||
|
||||
central_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(central_config_entry.entry_id)
|
||||
assert central_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity: ThermostatOverClimate = search_entity(
|
||||
hass, "climate.thecentralconfigmockname", "climate"
|
||||
)
|
||||
|
||||
assert entity is None
|
||||
|
||||
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
|
||||
|
||||
# Test that VTherm API find the CentralConfig
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
# Test that VTherm API have any central boiler entities
|
||||
assert api.nb_active_device_for_boiler_entity is not None
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: switch1.entity_id,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity.underlying_entities[0].entity_id == "switch.switch1"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
)
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 1. start a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch1.async_turn_on()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
|
||||
# Sometimes this test fails
|
||||
# mock_service_call.assert_has_calls(
|
||||
# [
|
||||
# call.service_call(
|
||||
# "switch",
|
||||
# "turn_on",
|
||||
# service_data={},
|
||||
# target={"entity_id": "switch.pompe_chaudiere"},
|
||||
# ),
|
||||
# ]
|
||||
# )
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": True},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 2. stop a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch1.async_turn_off()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": False},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_multiple(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
|
||||
switch2 = MockSwitch(hass, "switch2", "theSwitch2")
|
||||
switch3 = MockSwitch(hass, "switch3", "theSwitch3")
|
||||
switch4 = MockSwitch(hass, "switch4", "theSwitch4")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: switch1.entity_id,
|
||||
CONF_HEATER_2: switch2.entity_id,
|
||||
CONF_HEATER_3: switch3.entity_id,
|
||||
CONF_HEATER_4: switch4.entity_id,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity.underlying_entities[0].entity_id == "switch.switch1"
|
||||
assert entity.underlying_entities[1].entity_id == "switch.switch2"
|
||||
assert entity.underlying_entities[2].entity_id == "switch.switch3"
|
||||
assert entity.underlying_entities[3].entity_id == "switch.switch4"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 0. set threshold to 3
|
||||
api.nb_active_device_for_boiler_threshold_entity.set_native_value(3)
|
||||
assert api.nb_active_device_for_boiler_threshold == 3
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
)
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 1. start a first heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch1.async_turn_on()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 2. start a 2nd heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch2.async_turn_on()
|
||||
switch2.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler. Caution: each time a underlying heater state change itself,
|
||||
# the cycle restarts. So it is always the first heater that is started
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler == 2
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 3. start a 3rd heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch3.async_turn_on()
|
||||
switch3.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 2
|
||||
# No switch of the boiler. Caution: each time a underlying heater state change itself,
|
||||
# the cycle restarts. So it is always the first heater that is started
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": True},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 3
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 4. start a 4th heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch4.async_turn_on()
|
||||
switch4.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler. Caution: each time a underlying heater state change itself,
|
||||
# the cycle restarts. So it is always the first heater that is started
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
assert api.nb_active_device_for_boiler == 4
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 5. stop a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch1.async_turn_off()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
assert api.nb_active_device_for_boiler == 3
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 6. stop a 2nd heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await switch4.async_turn_off()
|
||||
switch4.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": False},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 2
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple_valve(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
valve1 = MockNumber(hass, "valve1", "theValve1")
|
||||
|
||||
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_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_VALVE: valve1.entity_id,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theovervalvemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverValveMockName"
|
||||
assert entity.is_over_valve
|
||||
assert entity.underlying_entities[0].entity_id == "number.valve1"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
)
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 1. start a valve
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
# we have to simulate the valve also else the test don't work
|
||||
valve1.set_native_value(10)
|
||||
valve1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": True},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 2. stop a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await send_temperature_change_event(entity, 25, now)
|
||||
# Change the valve value to 0
|
||||
valve1.set_native_value(0)
|
||||
valve1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": False},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple_climate(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
climate1 = MockClimate(hass, "climate1", "theClimate1")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
):
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate
|
||||
assert entity.underlying_entities[0].entity_id == "climate.climate1"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
)
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 1. start a climate
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
# we have to simulate the climate also else the test don't work
|
||||
climate1.set_hvac_mode(HVACMode.HEAT)
|
||||
climate1.set_hvac_action(HVACAction.HEATING)
|
||||
climate1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": True},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 2. stop a climate
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event:
|
||||
await send_temperature_change_event(entity, 25, now)
|
||||
climate1.set_hvac_mode(HVACMode.HEAT)
|
||||
climate1.set_hvac_action(HVACAction.IDLE)
|
||||
climate1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": False},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
@@ -31,8 +31,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(
|
||||
@@ -76,6 +76,7 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -96,8 +97,15 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
# Test that VTherm API doesn't have any central boiler entities
|
||||
assert api.nb_active_device_for_boiler_entity is None
|
||||
assert api.nb_active_device_for_boiler is None
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is None
|
||||
assert api.nb_active_device_for_boiler_threshold is None
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@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
|
||||
@@ -116,6 +124,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,
|
||||
@@ -157,8 +166,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
|
||||
@@ -172,9 +182,11 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
assert entity._security_default_on_percent == 0.1
|
||||
assert entity.is_inversed
|
||||
|
||||
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
|
||||
):
|
||||
@@ -192,6 +204,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,
|
||||
@@ -247,8 +260,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",
|
||||
@@ -268,7 +282,7 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity._security_default_on_percent == 0.1
|
||||
assert entity.is_inversed is False
|
||||
|
||||
assert entity.is_window_auto_enabled is True
|
||||
assert entity.is_window_auto_enabled is False # we have an entity_id
|
||||
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
|
||||
assert entity._window_delay_sec == 30
|
||||
assert entity._window_auto_close_threshold == 0.1
|
||||
@@ -286,9 +300,11 @@ async def test_full_over_switch_wo_central_config(
|
||||
|
||||
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
|
||||
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
|
||||
):
|
||||
@@ -306,6 +322,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,
|
||||
@@ -357,8 +374,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",
|
||||
@@ -377,7 +395,8 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity._security_default_on_percent == 0.2
|
||||
assert entity.is_inversed is False
|
||||
|
||||
assert entity.is_window_auto_enabled is True
|
||||
# We have an entity so window auto is not enabled
|
||||
assert entity.is_window_auto_enabled is False
|
||||
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
|
||||
assert entity._window_delay_sec == 15
|
||||
assert entity._window_auto_close_threshold == 1
|
||||
@@ -395,9 +414,11 @@ async def test_full_over_switch_with_central_config(
|
||||
|
||||
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
|
||||
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_over_switch_with_central_config_but_no_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
|
||||
):
|
||||
|
||||
1040
tests/test_central_mode.py
Normal file
@@ -143,6 +143,8 @@ async def test_user_config_flow_over_switch(
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USE_CENTRAL_MODE: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
}
|
||||
)
|
||||
assert result["result"]
|
||||
@@ -239,6 +241,7 @@ async def test_user_config_flow_over_climate(
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
@@ -287,6 +290,7 @@ async def test_user_config_flow_window_auto_ok(
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -363,6 +367,7 @@ async def test_user_config_flow_window_auto_ok(
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_WINDOW_DELAY: 30, # the default value is added
|
||||
CONF_USE_CENTRAL_MODE: True, # True is the defaulf value
|
||||
} | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
@@ -372,6 +377,7 @@ async def test_user_config_flow_window_auto_ok(
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
@@ -510,6 +516,8 @@ async def test_user_config_flow_over_4_switches(
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
}
|
||||
|
||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
|
||||
@@ -472,7 +472,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation of one underlying
|
||||
"""Test that when multiple climate are configured the activation of one underlying
|
||||
climate activate the others"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
@@ -541,11 +541,15 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
# Stop heating on one underlying climate
|
||||
# All underlying supposed to be aligned with the hvac_mode now
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
) as mock_underlying_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
||||
HVACMode.HEAT,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event(
|
||||
@@ -555,6 +559,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
@@ -577,6 +582,9 @@ async def test_multiple_climates_underlying_changes(
|
||||
# a function but a property
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
|
||||
HVACAction.IDLE,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
||||
HVACMode.OFF,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
@@ -601,6 +609,113 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_climates_underlying_changes_not_aligned(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple climate are configured the activation of one underlying
|
||||
climate don't activate the others if their havc_mode are not aligned"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4ClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4ClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "switch.mock_climate1",
|
||||
CONF_CLIMATE_2: "switch.mock_climate2",
|
||||
CONF_CLIMATE_3: "switch.mock_climate3",
|
||||
CONF_CLIMATE_4: "switch.mock_climate4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4climatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.nb_underlying_entities == 4
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.HEAT),
|
||||
]
|
||||
)
|
||||
|
||||
# Stop heating on one underlying climate
|
||||
# All underlying supposed to be aligned with the hvac_mode now
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
||||
HVACMode.COOL,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event(
|
||||
entity,
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 0
|
||||
# mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
# [
|
||||
# call.set_hvac_mode(HVACMode.OFF),
|
||||
# ]
|
||||
# )
|
||||
# No change
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_switch_power_management(
|
||||
|
||||
@@ -288,7 +288,7 @@ async def test_security_feature_back_on_percent(
|
||||
assert entity.security_state is False
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 3. Set security mode with a preset change
|
||||
# 3. Set safety mode with a preset change
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -400,7 +400,7 @@ async def test_security_over_climate(
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -471,7 +471,7 @@ async def test_security_over_climate(
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force security mode
|
||||
# Force safety mode
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (
|
||||
@@ -505,7 +505,7 @@ async def test_security_over_climate(
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False because a climate is never in security mode
|
||||
# Should stay False because a climate is never in safety mode
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
@@ -129,7 +129,7 @@ async def test_over_switch_ac_full_start(
|
||||
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.target_temperature == 16 # eco_ac_away
|
||||
assert entity.target_temperature == 27 # eco_ac_away (no change)
|
||||
|
||||
# Close a window
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
|
||||
@@ -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
|
||||
@@ -283,6 +283,18 @@ async def test_over_valve_full_start(
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0)
|
||||
expected_state = State(
|
||||
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get", return_value=expected_state
|
||||
):
|
||||
# Open a window
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
@@ -309,9 +321,235 @@ async def test_over_valve_full_start(
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert (
|
||||
entity.hvac_action is HVACAction.OFF
|
||||
or entity.hvac_action is HVACAction.IDLE
|
||||
)
|
||||
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 + "_temp": 7,
|
||||
PRESET_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=unused-argument, line-too-long, protected-access
|
||||
# pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
import logging
|
||||
@@ -6,6 +6,9 @@ from unittest.mock import patch, call, PropertyMock
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
@@ -46,6 +49,7 @@ async def test_window_management_time_not_enough(
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -134,6 +138,7 @@ async def test_window_management_time_enough(
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -242,7 +247,7 @@ async def test_window_management_time_enough(
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Window management"""
|
||||
"""Test the auto Window management with fast slope down"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -295,6 +300,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled is True
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
@@ -431,6 +437,125 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_fast_and_sensor(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test that the auto-window detection algorithm is deactivated if a window sensor is provided"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled is False
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater don't turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert entity.is_device_active is True
|
||||
assert entity.last_temperature_slope == 0.0
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0 # no change in heater
|
||||
assert mock_heater_off.call_count == 0 # no change in heater
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
# The window open should be detected (but not used)
|
||||
# because we need to calculate the slope anyway, we have the algorithm running
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -461,7 +586,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 1, # 0 will deactivate window auto detection
|
||||
},
|
||||
)
|
||||
|
||||
@@ -484,8 +609,9 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled is True
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
# 1. Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
@@ -493,7 +619,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# Make the temperature down
|
||||
# 2. Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -513,7 +639,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# send one degre down in one minute
|
||||
# 3. send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -549,12 +675,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
|
||||
# This is to avoid that the slope stayx under 6, else we will reactivate the window immediatly
|
||||
# 4. This is to avoid that the slope stay under 6, else we will reactivate the window immediatly
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp, sleep=False)
|
||||
dearm_window_auto = await send_temperature_change_event(
|
||||
entity, 19, event_timestamp, sleep=False
|
||||
)
|
||||
assert entity.last_temperature_slope > -6.0
|
||||
|
||||
# Waits for automatic disable
|
||||
# 5. Waits for automatic disable
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -563,7 +691,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await asyncio.sleep(0.3)
|
||||
# simulate the expiration of the delay
|
||||
await dearm_window_auto(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
@@ -698,7 +827,6 @@ async def test_window_auto_no_on_percent(
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
# PR - Adding Window Bypass
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -751,6 +879,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled is False
|
||||
|
||||
# change temperature to force turning on the heater
|
||||
with patch(
|
||||
@@ -867,7 +996,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 1, # Should be > 0 to activate window_auto
|
||||
},
|
||||
)
|
||||
|
||||
@@ -890,6 +1019,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled
|
||||
|
||||
# Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
@@ -1081,3 +1211,694 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Window management with the fan_only option"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 1,
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
|
||||
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
# CONF_WINDOW_AUTO_MAX_DURATION: 1, # 0 will deactivate window auto detection
|
||||
},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mockUniqueId",
|
||||
name="MockClimateName",
|
||||
hvac_modes=[HVACMode.HEAT, HVACMode.COOL, HVACMode.FAN_ONLY],
|
||||
)
|
||||
|
||||
# 1. intialize climate entity
|
||||
with patch(
|
||||
"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"
|
||||
)
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.window_action == CONF_WINDOW_FAN_ONLY
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
assert entity.target_temperature == 18
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# 2. Open the window, condition of time is satisfied, check the thermostat and heater turns off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.FAN_ONLY}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# The underlying should be in OFF hvac_mode
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.FAN_ONLY),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
# The underlying should be in FAN_ONLY hvac_mode
|
||||
assert entity.hvac_mode is HVACMode.FAN_ONLY
|
||||
assert entity._saved_hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 3. Close the window
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_function = await send_window_change_event(
|
||||
entity, False, True, event_timestamp, sleep=False
|
||||
)
|
||||
|
||||
await try_function(None)
|
||||
|
||||
# Wait for initial delay of heater
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||
),
|
||||
],
|
||||
any_order=False,
|
||||
)
|
||||
|
||||
# The underlying should be in OFF hvac_mode
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.HEAT),
|
||||
]
|
||||
)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_action_fan_only_ko(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Window management with the fan_only option but the underlyings doesn't have the FAN_ONLY mode
|
||||
So the VTherm switch to OFF which is the fallback mode"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 1,
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
|
||||
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
|
||||
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
|
||||
# CONF_WINDOW_AUTO_MAX_DURATION: 1, # 0 will deactivate window auto detection
|
||||
},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mockUniqueId",
|
||||
name="MockClimateName",
|
||||
hvac_modes=[HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO],
|
||||
)
|
||||
|
||||
# 1. intialize climate entity
|
||||
with patch(
|
||||
"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"
|
||||
)
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.window_action == CONF_WINDOW_FAN_ONLY
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
assert entity.target_temperature == 18
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# 2. Open the window, condition of time is satisfied, check the thermostat and heater turns off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
|
||||
)
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# The underlying should be in OFF hvac_mode
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity._saved_hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 3. Close the window
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_function = await send_window_change_event(
|
||||
entity, False, True, event_timestamp, sleep=False
|
||||
)
|
||||
|
||||
await try_function(None)
|
||||
|
||||
# Wait for initial delay of heater
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||
),
|
||||
],
|
||||
any_order=False,
|
||||
)
|
||||
|
||||
# The underlying should be in OFF hvac_mode
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.HEAT),
|
||||
]
|
||||
)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Window management with the eco_temp option"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_ECO_TEMP,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled is True
|
||||
|
||||
# 1. Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# 2. Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_auto_state is STATE_OFF
|
||||
|
||||
# 3. send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.window_state == STATE_OFF
|
||||
# No change on HVACMode
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No change on preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
# The eco temp
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": -6.24},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
# 4. send another 0.1 degre in one minute -> no change
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 17.9, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == -7.49
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No change on preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
# The eco temp
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
# 5. send another plus 1.1 degre in one minute -> restore state
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"cause": "end of slope alert",
|
||||
"curve_slope": 0.42,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == 0.42
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No change on preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
# The eco temp
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Window management with the frost_temp option"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
"frost_temp": 10,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.is_window_auto_enabled is True
|
||||
|
||||
# 1. Initialize the slope algo with 2 measurements
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# 2. Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_auto_state is STATE_OFF
|
||||
|
||||
# 3. send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == -6.24
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.window_state == STATE_OFF
|
||||
# No change on HVACMode
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No change on preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
# The eco temp
|
||||
assert entity.target_temperature == 10
|
||||
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": -6.24},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
# 4. send another 0.1 degre in one minute -> no change
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 17.9, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == -7.49
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No change on preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
# The eco temp
|
||||
assert entity.target_temperature == 10
|
||||
|
||||
# 5. send another plus 1.1 degre in one minute -> restore state
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"cause": "end of slope alert",
|
||||
"curve_slope": 0.42,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == 0.42
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No change on preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
# The eco temp
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||